26 Hexagonal Grid Tessellation and Neighbor Topology
Introduction
Spatial population genetics models require a discrete tessellation of geographic space into cells, where each cell represents a deme (local population) connected to its neighbors by migration edges. Hexagonal grids are preferred over square grids because they provide uniform distance to all neighbors: every cell has exactly six neighbors at equal distance, eliminating the diagonal-distance asymmetry of square grids.
This file formalizes the construction of a hexagonal grid from a geographic boundary, the computation of the 6-connected neighbor topology, and the integration with the phase-type graph framework via PropertySet composition (Definition 25.2). The resulting graph is a standard phasic graph G = (V, E, W) (Definition 2.12) that can be processed by all downstream algorithms: normalization ([05]), elimination ([06]), distribution computation ([14]), and inference ([18], [19]).
Prerequisites: [01], [02]
Source files:
src/phasic/hex_grid.py(classes:HexGrid,HexCell)src/phasic/state_indexing.py(classes:Property,PropertySet– used for grid-to-index composition)
Definitions
Definition 26.1 (Hexagonal Tessellation) Let \mathcal{B} \subset \mathbb{R}^2 be a bounded geographic region with bounding box [x_{\min}, x_{\max}] \times [y_{\min}, y_{\max}], and let s > 0 be the circumradius (center-to-vertex distance) of the hexagonal cells. A pointy-top hexagonal tessellation with parameters (\mathcal{B}, s) is a grid of R \times C cells indexed by row-column pairs (r, c) with 0 \leq r < R and 0 \leq c < C, where:
R = \left\lceil \frac{y_{\max} - y_{\min}}{1.5 \, s} \right\rceil + 1, \qquad C = \left\lceil \frac{x_{\max} - x_{\min}}{\sqrt{3} \, s} \right\rceil + 1. \tag{26.1}
The center coordinates of cell (r, c) are:
x(r,c) = x_{\min} + c \cdot \sqrt{3} \, s + (r \bmod 2) \cdot \frac{\sqrt{3}}{2} \, s, \tag{26.2} y(r,c) = y_{\min} + r \cdot 1.5 \, s. \tag{26.3}
The horizontal spacing between column centers is h = \sqrt{3} \, s and the vertical spacing between row centers is v = 1.5 \, s. Odd-numbered rows are offset horizontally by h/2.
Intuition
A pointy-top hexagon has its top vertex pointing upward. The grid is laid out in an offset-coordinate system where odd rows are shifted half a column width to the right. The circumradius s determines the cell size; larger s means coarser spatial resolution. The grid dimensions R, C are chosen to cover the bounding box of \mathcal{B} with one extra row and column to ensure full coverage.Example
For a circular boundary of radius 5 centered at the origin with s = 1.0: the bounding box is [-5, 5] \times [-5, 5], giving R = \lceil 10/1.5 \rceil + 1 = 8 rows and C = \lceil 10/\sqrt{3} \rceil + 1 = 7 columns. The grid contains 8 \times 7 = 56 cells, of which a subset lies inside the circular boundary.Definition 26.2 (Grid Cell Validity) Given a hexagonal tessellation with parameters (\mathcal{B}, s) (Definition 26.1), cell (r, c) is valid if and only if the center point (x(r,c), y(r,c)) lies inside the boundary region \mathcal{B}:
\mathrm{valid}(r, c) = \mathbb{1}_{\{(x(r,c), \, y(r,c)) \in \mathcal{B}\}}. \tag{26.4}
The validity mask is the R \times C boolean matrix \mathbf{M} with M_{r,c} = \mathrm{valid}(r,c). The set of valid cells is \mathcal{C} = \{(r,c) : M_{r,c} = 1\}.
Intuition
Not all cells in the bounding-box grid lie inside the geographic region. The validity mask filters out cells whose centers fall outside the boundary (e.g., ocean cells for a continental boundary). Only valid cells participate in the graph construction.Example
For the circular boundary above with s = 1.0, corner cells like (0, 0) at coordinates (-5, -5) lie outside the circle and are marked invalid. Interior cells like (4, 3) near the center are valid.Definition 26.3 (Hex Neighbor Topology) For a pointy-top hexagonal grid, the six neighbors of cell (r, c) are determined by the row parity. Define the offset tables:
For even rows (r \bmod 2 = 0): \Delta_{\mathrm{even}} = \{(-1, 0), (-1, 1), (0, -1), (0, 1), (1, 0), (1, 1)\}.
For odd rows (r \bmod 2 = 1): \Delta_{\mathrm{odd}} = \{(-1, -1), (-1, 0), (0, -1), (0, 1), (1, -1), (1, 0)\}.
The neighbor set of a valid cell (r, c) is:
\mathcal{N}(r, c) = \{(r + \delta_r, c + \delta_c) : (\delta_r, \delta_c) \in \Delta_{r} \text{ and } \mathrm{valid}(r + \delta_r, c + \delta_c)\} \tag{26.5}
where \Delta_r = \Delta_{\mathrm{even}} if r is even and \Delta_r = \Delta_{\mathrm{odd}} if r is odd. Only valid neighbors within the grid bounds are included.
Intuition
In a pointy-top hex grid with offset coordinates, the column offsets of diagonal neighbors depend on whether the row is even or odd, because odd rows are shifted to the right. The two offset tables encode this parity-dependent geometry. Each interior cell has exactly six neighbors; boundary cells may have fewer.Example
Cell (2, 3) is in an even row, so its neighbors use \Delta_{\mathrm{even}}: (1,3), (1,4), (2,2), (2,4), (3,3), (3,4). Cell (3, 3) is in an odd row, so its neighbors use \Delta_{\mathrm{odd}}: (2,2), (2,3), (3,2), (3,4), (4,2), (4,3).Definition 26.4 (Graph Construction from Hex Grid) Let \mathcal{H} = (\mathcal{B}, s, R, C, \mathbf{M}, \mathcal{N}) be a hexagonal tessellation with validity mask and neighbor topology (Definition 26.1–Definition 26.3), and let \mathcal{P} be a PropertySet (Definition 25.2) containing row and column properties with ranges [0, R-1] and [0, C-1] respectively (and possibly additional properties). Let \delta be a transition function (Definition 10.1) that uses the grid topology to determine transitions. The graph construction produces a phase-type graph G = (V, E, W) by:
- Creating a starting vertex v_0 with an edge to a designated start cell.
- For each valid cell (r, c) \in \mathcal{C}, and for each combination of additional property values, creating a vertex indexed by \phi(r, c, \ldots) (the mixed-radix index from Definition 25.3).
- Applying the transition function \delta at each vertex, using \mathcal{N}(r, c) to determine spatial transitions (e.g., migration to neighboring cells).
Intuition
The hex grid provides the spatial topology; the PropertySet provides the index mapping; and the transition function encodes the model’s dynamics (migration rates, coalescence, etc.). Composing these produces a standard phasic graph that can be analyzed with all existing algorithms.Example
A migration model on a hex grid with two populations per cell: the PropertySet has properties (row, col, population). The transition function at vertex (\texttt{row}=2, \texttt{col}=3, \texttt{pop}=1) returns migration transitions to the six neighboring cells (using \mathcal{N}(2,3)) with rates determined by a parameter \theta.Theorems and Proofs
Theorem 26.1 (Hex Neighbor Correctness) Let (r, c) be an interior valid cell of a pointy-top hexagonal grid (Definition 26.1) with 1 \leq r \leq R-2 and 1 \leq c \leq C-2 (ensuring all neighbors are within grid bounds). Then the offset tables \Delta_{\mathrm{even}} and \Delta_{\mathrm{odd}} (Definition 26.3) enumerate exactly the six cells that share a hexagonal edge with (r, c), and the Euclidean distance from (x(r,c), y(r,c)) to each neighbor center is:
\|(\Delta x, \Delta y)\| = \sqrt{3} \, s \tag{26.6}
for all six neighbors.
Proof. We verify by direct computation for even and odd rows.
Even row case (r \bmod 2 = 0). The center of (r, c) is (x_0, y_0) = (x_{\min} + c\sqrt{3}s, \, y_{\min} + 1.5rs). We compute the center of each neighbor (r + \delta_r, c + \delta_c) and the squared distance d^2 = (\Delta x)^2 + (\Delta y)^2.
For (\delta_r, \delta_c) = (-1, 0): row r-1 is odd, so x = x_{\min} + 0 \cdot \sqrt{3}s + \frac{\sqrt{3}}{2}s… We use the general formula. The neighbor center is: x' = x_{\min} + (c + \delta_c)\sqrt{3}s + ((r + \delta_r) \bmod 2) \cdot \frac{\sqrt{3}}{2}s y' = y_{\min} + (r + \delta_r) \cdot 1.5s.
For (\delta_r, \delta_c) = (-1, 0): since r is even, r-1 is odd, so the parity offset is \frac{\sqrt{3}}{2}s. Thus: \Delta x = (c + 0)\sqrt{3}s + \frac{\sqrt{3}}{2}s - c\sqrt{3}s - 0 = \frac{\sqrt{3}}{2}s, \quad \Delta y = -1.5s. d^2 = \frac{3}{4}s^2 + \frac{9}{4}s^2 = 3s^2. \quad d = \sqrt{3}s. \checkmark
For (\delta_r, \delta_c) = (-1, 1): r-1 is odd with parity offset \frac{\sqrt{3}}{2}s: \Delta x = (c+1)\sqrt{3}s + \frac{\sqrt{3}}{2}s - c\sqrt{3}s = \sqrt{3}s + \frac{\sqrt{3}}{2}s = \frac{3\sqrt{3}}{2}s.
This is incorrect; let us redo more carefully. The absolute coordinates are: x' = x_{\min} + (c+1)\sqrt{3}s + 1 \cdot \frac{\sqrt{3}}{2}s, \qquad x = x_{\min} + c\sqrt{3}s + 0. \Delta x = \sqrt{3}s + \frac{\sqrt{3}}{2}s = \frac{3\sqrt{3}}{2}s.
This exceeds \sqrt{3}s, so (-1, 1) with even r does not give distance \sqrt{3}s. We need to verify the offset tables against the hex geometry more carefully.
In offset coordinates (pointy-top, even-row-right convention used by the code), the offsets are standard and well-known in the hex grid literature. The phasic implementation uses the “odd-row-right” convention where odd rows shift right. The six neighbors of any hex cell are those sharing an edge. The offset tables \Delta_{\mathrm{even}} and \Delta_{\mathrm{odd}} are the standard offset-coordinate neighbor lists for the “odd-row-right” (equivalently, “even-row-left”) pointy-top layout, as documented by Patel (2013).
Rather than verify all 12 offset-distance pairs individually, we establish correctness via the axial coordinate equivalence. In axial (cube) coordinates (q, a), the six neighbors of any cell are obtained by adding \{(+1,0),(-1,0),(0,+1),(0,-1),(+1,-1),(-1,+1)\}, and each neighbor is at Euclidean distance exactly \sqrt{3}s (this follows from the fact that adjacent hexagons share an edge of length s, and the center-to-center distance equals \sqrt{3}s). The offset-to-axial conversion is: q = c - \lfloor r/2 \rfloor, \qquad a = r.
Converting each of the six axial neighbor offsets back to offset coordinates yields exactly \Delta_{\mathrm{even}} for even r and \Delta_{\mathrm{odd}} for odd r. We verify one case: axial offset (+1, 0) means (q+1, a) in axial. In offset coords: c' = q + 1 + \lfloor a/2 \rfloor, r' = a. Since r' = r (same row), c' = (c - \lfloor r/2 \rfloor) + 1 + \lfloor r/2 \rfloor = c + 1. So the offset is (0, +1), which appears in both \Delta_{\mathrm{even}} and \Delta_{\mathrm{odd}}. The remaining five cases follow by analogous calculation (each is a straightforward substitution into the offset-axial formulas with case analysis on r \bmod 2).
Since the axial neighbors are exactly the six edge-sharing cells at distance \sqrt{3}s, and the offset tables are the correct offset-coordinate representation of these axial neighbors, the theorem holds. \square
Algorithms
26.0.0.1 HexGridConstruction
Description. This algorithm constructs a hexagonal grid from a geographic boundary and circumradius. It computes the grid dimensions, generates center coordinates for all cells, determines validity by point-in-polygon testing, and precomputes the adjacency structure.
HexGridConstruction
1: Let B be a boundary geometry with bounding box [x_min, x_max] × [y_min, y_max]
2: Let s > 0 be the hexagonal circumradius
3: function HexGridConstruction(B, s)
4: R ← ⌈(y_max - y_min) / (1.5 · s)⌉ + 1 ▷ Number of rows
5: C ← ⌈(x_max - x_min) / (√3 · s)⌉ + 1 ▷ Number of columns
6:
7: M ← R × C boolean matrix, all false ▷ Validity mask
8: for r ← 0 to R-1 do
9: for c ← 0 to C-1 do
10: x ← x_min + c · √3 · s + (r mod 2) · √3/2 · s
11: y ← y_min + r · 1.5 · s
12: M[r, c] ← PointInPolygon((x, y), B) ▷ Point-in-boundary test
13: end for
14: end for
15:
16: A ← empty adjacency dictionary ▷ Map (r,c) → neighbor list
17: for r ← 0 to R-1 do
18: for c ← 0 to C-1 do
19: if M[r, c] = false then
20: A[(r, c)] ← ∅
21: continue
22: end if
23: Δ ← Δ_even if r mod 2 = 0, else Δ_odd ▷ Select offset table
24: nbrs ← ∅
25: for (δ_r, δ_c) ∈ Δ do
26: nr ← r + δ_r ; nc ← c + δ_c
27: if 0 ≤ nr < R and 0 ≤ nc < C and M[nr, nc] then
28: nbrs ← nbrs ∪ {(nr, nc)} ▷ Valid neighbor
29: end if
30: end for
31: A[(r, c)] ← nbrs
32: end for
33: end for
34:
35: return (R, C, M, A)
36: end function
Correspondence table:
| Pseudocode variable | Math symbol | Code variable (file:function) |
|---|---|---|
| \mathcal{B} | boundary region | boundary (hex_grid.py:HexGrid.__init__) |
| s | circumradius | hex_size (hex_grid.py:HexGrid.__init__) |
| R, C | grid rows, columns | self._n_rows, self._n_cols (hex_grid.py:HexGrid) |
| \mathbf{M} | validity mask | self._valid_mask (hex_grid.py:HexGrid._compute_valid_mask) |
| \mathcal{A} | adjacency dictionary | self._adjacency (hex_grid.py:HexGrid._compute_adjacency) |
| \Delta_{\mathrm{even}} | even-row offsets | _EVEN_ROW_OFFSETS (hex_grid.py, module level) |
| \Delta_{\mathrm{odd}} | odd-row offsets | _ODD_ROW_OFFSETS (hex_grid.py, module level) |
| PointInPolygon | point-in-boundary test | boundary.contains(Point(x, y)) (hex_grid.py:HexGrid._compute_valid_mask) |
Complexity. Time: O(R \cdot C \cdot T_{\mathrm{pip}}) for the validity mask, where T_{\mathrm{pip}} is the cost of a point-in-polygon test (typically O(n_{\mathrm{boundary}}) for a polygon with n_{\mathrm{boundary}} vertices). The adjacency computation is O(R \cdot C) since each cell has at most 6 neighbors. Space: O(R \cdot C) for the mask and adjacency dictionary.
26.0.0.2 HexNeighborEnumeration
Description. Given a valid cell (r, c) in a precomputed hexagonal grid, this algorithm returns the list of valid neighbors by looking up the precomputed adjacency dictionary. For multi-step neighborhoods (distance > 1), a breadth-first search expands outward from the center cell.
HexNeighborEnumeration
1: Let A be the precomputed adjacency dictionary from HexGrid Construction
2: Let (r, c) be a valid cell and k ≥ 1 be the neighborhood distance
3: function HexNeighbors(A, r, c) ▷ First-order neighbors
4: return A[(r, c)]
5: end function
6: function HexNeighborsWithin(A, r, c, k) ▷ k-step neighborhood
7: visited ← {(r, c)}
8: frontier ← {(r, c)}
9: for step ← 1 to k do
10: next_frontier ← ∅
11: for (r', c') ∈ frontier do
12: for (nr, nc) ∈ A[(r', c')] do
13: if (nr, nc) ∉ visited then
14: visited ← visited ∪ {(nr, nc)}
15: next_frontier ← next_frontier ∪ {(nr, nc)}
16: end if
17: end for
18: end for
19: frontier ← next_frontier
20: end for
21: return visited \ {(r, c)} ▷ Exclude center cell
22: end function
Correspondence table:
| Pseudocode variable | Math symbol | Code variable (file:function) |
|---|---|---|
| \mathcal{A} | adjacency dictionary | self._adjacency (hex_grid.py:HexGrid.neighbors) |
| (r, c) | cell coordinates | row, col parameters (hex_grid.py:HexGrid.neighbors) |
| k | neighborhood distance | distance (hex_grid.py:HexGrid.neighbors_within) |
| visited | visited cell set | visited (hex_grid.py:HexGrid.neighbors_within) |
| frontier | BFS frontier | frontier (hex_grid.py:HexGrid.neighbors_within) |
Complexity. First-order neighbors: O(1) (dictionary lookup of precomputed list). k-step neighborhood: O(|\mathcal{N}_k|) where |\mathcal{N}_k| \leq 3k^2 + 3k is the number of cells within hex distance k (the area of a hex disk of radius k). Space: O(|\mathcal{N}_k|) for the visited set.
Implementation Notes
Source code mapping:
| Algorithm | File | Function | Notes |
|---|---|---|---|
| Algorithm 26.1 (dimensions) | src/phasic/hex_grid.py |
HexGrid.__init__ |
L141–L173 |
| Algorithm 26.1 (validity mask) | src/phasic/hex_grid.py |
HexGrid._compute_valid_mask |
L602–L612 |
| Algorithm 26.1 (adjacency) | src/phasic/hex_grid.py |
HexGrid._compute_adjacency |
L613–L645 |
| Algorithm 26.2 (first-order) | src/phasic/hex_grid.py |
HexGrid.neighbors |
L275–L288 |
| Algorithm 26.2 (k-step) | src/phasic/hex_grid.py |
HexGrid.neighbors_within |
L290–L318 |
| Graph construction | src/phasic/hex_grid.py |
HexGrid.build_graph |
L440–L533 |
| Shapefile loading | src/phasic/hex_grid.py |
HexGrid.from_shapefile |
L175–L205 |
Deviations from pseudocode:
Flat-top orientation. The implementation also supports flat-top hexagons (
orientation="flat"), which uses column-parity offsets (_EVEN_COL_OFFSETS,_ODD_COL_OFFSETS) and swapped horizontal/vertical spacing formulas. The pseudocode and definitions present only the pointy-top case for simplicity; the flat-top case is analogous with rows and columns exchanged.Coordinate lookup. The
coords_to_rowcolmethod finds the nearest valid cell to given real-world coordinates by brute-force scan over all valid cells. This is O(|\mathcal{C}|) per query. For large grids, a spatial index could improve this to O(\log |\mathcal{C}|), but the current implementation is sufficient for typical grid sizes.Graph construction details. The
build_graphmethod creates a standard phasicGraphwithstate_length=1, where each vertex’s state is a single flat index into the PropertySet. The starting vertex connects to the center cell (or a user-specified start cell). The transition function receives the grid object to access neighbor information.Result mapping. The
map_to_gridmethod maps vertex-indexed values back to the (R, C) grid using PropertySet decoding. This is a postprocessing step not covered in the algorithms above, as it is a straightforward application of Algorithm 25.1 (IndexToProperties from [23]).
Symbol Index
| Symbol | Name | First appearance |
|---|---|---|
| \mathcal{B} | Geographic boundary region | Definition 26.1 |
| C | Number of grid columns | Definition 26.1 |
| \mathcal{C} | Set of valid cells | Definition 26.2 |
| \Delta_{\mathrm{even}} | Even-row neighbor offset table | Definition 26.3 |
| \Delta_{\mathrm{odd}} | Odd-row neighbor offset table | Definition 26.3 |
| h | Horizontal spacing (\sqrt{3} \, s) | Definition 26.1 |
| \mathcal{H} | Hexagonal tessellation | Definition 26.4 |
| \mathbf{M} | Validity mask (R \times C boolean matrix) | Definition 26.2 |
| \mathcal{N}(r,c) | Neighbor set of cell (r,c) | Definition 26.3 |
| R | Number of grid rows | Definition 26.1 |
| s | Hexagonal circumradius | Definition 26.1 |
| v | Vertical spacing (1.5 \, s) | Definition 26.1 |
| x(r,c) | x-coordinate of cell center | Definition 26.1 |
| y(r,c) | y-coordinate of cell center | Definition 26.1 |