Skip to content

Commit bb50b49

Browse files
authored
Merge pull request #92 from tmillenaar/tiles
Add Tiles class
2 parents 372b95d + a4d3a48 commit bb50b49

17 files changed

Lines changed: 845 additions & 153 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ name = "gridkit_rs"
99
crate-type = ["cdylib"]
1010

1111
[dependencies]
12+
enum_delegate = "0.2.0"
1213
geo-types = "0.7.12"
13-
numpy = "0.19.0"
14-
pyo3 = "0.19.0"
14+
numpy = "0.21.0"
15+
pyo3 = "0.21.2"
1516
wkb = "0.7.1"

docs/source/release_notes.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
Release notes
44
================
55

6+
Version 0.14.0 (August 11, 2024)
7+
--------------------------------
8+
Featrures
9+
- A new :class:`.Tile` class that references a set of cells and has some convenience methods
10+
that describe the tile, such as :attr:`.Tile.indices` and :attr:`.Tile.corners`.
11+
This class is takes a similar role to the :meth:`.BaseGrid.cells_in_bounds` method,
12+
but is able to work with rotated grids. The intent is that in the long run a DataTile
13+
will replace the BoundedGrid for this reason.
14+
15+
Documentation
16+
- Add example :ref:`tiles.py <example tiles>` which explains the usage of the new :class:`.Tile` class.
17+
- Use more neighbours in example :ref:`flower_of_life.py <example flower of life>` since the final flower
18+
was missing some circles in the bottom left.
19+
20+
Misc
21+
- Rename the PyO3 classes PyTriGrid, PyRectGrid and PyHexGrid to PyO3TriGrid, PyO3RectGrid and PyO3HexGrid, respectively.
22+
This is done to avoid confusion. From the Rust perspective these represent Python classes but from the Python perspective
23+
these represent Rust classes. PyO3 seems to be less ambiguous for it makes sense from both perspectives.
24+
625
Version 0.13.0 (July 10, 2024)
726
-----------------------------
827
Features

examples/grid_definitions/tiles.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
.. _example tiles:
3+
4+
Grid Tiles
5+
==========
6+
7+
Convenient way to refer to a collection of cells in a grid.
8+
9+
Introduction
10+
------------
11+
12+
The Tile object is designed to be an easy way to refer to a collection of cells on a grid.
13+
In order to create one, we supply it the grid is is associated with,
14+
the starting cell id in the bottom left and the number of cells in x and in y direction.
15+
16+
Let's plot the cells contained in a tile and show the tile outline.
17+
18+
"""
19+
20+
# sphinx_gallery_thumbnail_number = -2
21+
22+
from matplotlib import pyplot as plt
23+
from shapely.geometry import Polygon
24+
25+
from gridkit import HexGrid, Tile
26+
from gridkit.doc_utils import plot_polygons
27+
28+
grid = HexGrid(size=1, offset=(0.3, 0.5), rotation=18)
29+
30+
tile = Tile(
31+
grid=grid,
32+
start_id=[5, 5],
33+
nx=8,
34+
ny=5,
35+
)
36+
37+
geoms = grid.to_shapely(tile.indices, as_multipolygon=True)
38+
39+
bbox = Polygon(tile.corners())
40+
plot_polygons(geoms, fill=True, linewidth=2, alpha=0.3)
41+
plot_polygons(bbox, fill=False, linewidth=2, colors="red")
42+
plt.show()
43+
44+
# %%
45+
#
46+
# Note that the outline of the tile cuts through the cells on the left and right,
47+
# and note how the left side is missing coverage of some half-cells and on the right
48+
# some cells extend beyond the border. This is deliberate and is designed in a way where the
49+
# two neighboring tiles don't share any cells.
50+
# To demonstrate this, let's plot several tiles next to each other:
51+
#
52+
53+
tiles = [
54+
Tile(grid, [-3, 5], nx=8, ny=5), # 0
55+
Tile(grid, [5, 5], nx=8, ny=5), # 1
56+
Tile(grid, [5 + 8, 5], nx=8, ny=5), # 2
57+
Tile(grid, [-1, 0], nx=10, ny=5), # 3
58+
Tile(grid, [9, 0], nx=10, ny=5), # 4
59+
]
60+
61+
bboxes = [Polygon(t.corners()) for t in tiles]
62+
for i, (tile, color) in enumerate(
63+
zip(tiles, ["peru", "deepskyblue", "orange", "teal", "purple"])
64+
):
65+
geoms = tile.grid.to_shapely(
66+
tile.indices, as_multipolygon=True
67+
) # Note how I call tile.grid.to_shapely, read on for explenation
68+
plot_polygons(geoms, fill=True, linewidth=2, alpha=0.3, colors=color)
69+
center = tile.corners().mean(axis=0)
70+
plt.text(*center, i, size=30)
71+
plot_polygons(bboxes, fill=False, linewidth=2, colors="red")
72+
plt.show()
73+
74+
# %%
75+
#
76+
# This is set up such that multiple tiles can cover a larger plane together which
77+
# is usefull for distributed computing, though GridKit does not handle any of this
78+
# distribution itself. It is up to the user to use this tileability together with a tool
79+
# such as Dask or Multiporcessing.
80+
# This of course only works well if all tiles are based off the same grid.
81+
#
82+
# Note that every Tile has it's own copy of the grid. If we mutate the original grid,
83+
# the .grid associated with each tile remains unaffected.
84+
# Be mindful of what grid you refer to. It is easy to make the mistake where
85+
# the original grid is modified and the indices of the tile are used with the
86+
# ``grid`` variable instead of tile.grid.
87+
# Here I will show what it might look like if you use the tile indices on a different grid:
88+
#
89+
90+
grid.rotation -= 5
91+
92+
bboxes = [Polygon(t.corners()) for t in tiles]
93+
for i, (tile, color) in enumerate(
94+
zip(tiles, ["peru", "deepskyblue", "orange", "teal", "purple"])
95+
):
96+
geoms = grid.to_shapely(
97+
tile.indices, as_multipolygon=True
98+
) # Note how I call grid.to_shapely and not tile.grid.to_shapely!
99+
plot_polygons(geoms, fill=True, linewidth=2, alpha=0.3, colors=color)
100+
center = tile.corners().mean(axis=0)
101+
plt.text(*center, i, size=30)
102+
plot_polygons(bboxes, fill=False, linewidth=2, colors="red")
103+
plt.show()

examples/patterns/flower_of_life.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from gridkit import HexGrid
2323

2424
center = [0, 0]
25-
depth = 2
25+
depth = 3
2626

2727
grid = HexGrid(size=1, rotation=0).anchor(center)
2828

gridkit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
from gridkit.index import GridIndex, validate_index
55
from gridkit.io import read_raster, write_raster
66
from gridkit.rect_grid import BoundedRectGrid, RectGrid
7+
from gridkit.tile import Tile
78
from gridkit.tri_grid import BoundedTriGrid, TriGrid
89
from gridkit.version import __version__

gridkit/hex_grid.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from gridkit.base_grid import BaseGrid
88
from gridkit.bounded_grid import BoundedGrid
99
from gridkit.errors import AlignmentError, IntersectionError
10-
from gridkit.gridkit_rs import PyHexGrid, interp
10+
from gridkit.gridkit_rs import PyO3HexGrid, interp
1111
from gridkit.index import GridIndex, validate_index
1212
from gridkit.rect_grid import RectGrid
1313
from gridkit.tri_grid import TriGrid
@@ -123,7 +123,7 @@ def __init__(
123123
offset = offset[::-1]
124124

125125
self._shape = shape
126-
self._grid = PyHexGrid(cellsize=size, offset=offset, rotation=self._rotation)
126+
self._grid = PyO3HexGrid(cellsize=size, offset=offset, rotation=self._rotation)
127127
self.bounded_cls = BoundedHexGrid
128128
super(HexGrid, self).__init__(*args, **kwargs)
129129

@@ -770,7 +770,7 @@ def _update_inner_grid(self, size=None, offset=None, rotation=None):
770770
rotation = self.rotation
771771
if self.shape == "flat":
772772
rotation = -rotation
773-
return PyHexGrid(cellsize=size, offset=offset, rotation=rotation)
773+
return PyO3HexGrid(cellsize=size, offset=offset, rotation=rotation)
774774

775775
def update(
776776
self,

gridkit/rect_grid.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from gridkit.base_grid import BaseGrid
99
from gridkit.bounded_grid import BoundedGrid
1010
from gridkit.errors import AlignmentError, IntersectionError
11-
from gridkit.gridkit_rs import PyRectGrid
11+
from gridkit.gridkit_rs import PyO3RectGrid
1212
from gridkit.index import GridIndex, validate_index
1313

1414

@@ -110,7 +110,7 @@ def __init__(
110110
self._dx = dx
111111
self._dy = dy
112112
self._rotation = rotation
113-
self._grid = PyRectGrid(dx=dx, dy=dy, offset=tuple(offset), rotation=rotation)
113+
self._grid = PyO3RectGrid(dx=dx, dy=dy, offset=tuple(offset), rotation=rotation)
114114
self.bounded_cls = BoundedRectGrid
115115

116116
super(RectGrid, self).__init__(*args, **kwargs)
@@ -720,7 +720,7 @@ def _update_inner_grid(
720720
offset = self.offset
721721
if rotation is None:
722722
rotation = self.rotation
723-
return PyRectGrid(dx=dx, dy=dy, offset=offset, rotation=rotation)
723+
return PyO3RectGrid(dx=dx, dy=dy, offset=offset, rotation=rotation)
724724

725725
def update(
726726
self,

gridkit/tile.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from typing import Tuple, Union
2+
3+
import numpy
4+
5+
from gridkit.base_grid import BaseGrid
6+
from gridkit.gridkit_rs import PyO3HexTile, PyO3RectTile, PyO3TriTile
7+
from gridkit.hex_grid import HexGrid
8+
from gridkit.index import GridIndex
9+
from gridkit.rect_grid import RectGrid
10+
from gridkit.tri_grid import TriGrid
11+
12+
13+
class Tile:
14+
"""A Tile describes a set of cells defined by the ``start_id``,
15+
which is the cell that defines the bottom-left corner of the tile,
16+
and ``nx`` and ``ny``, which are the number of cells in the x and y directions, respectively.
17+
18+
Each tile is associated with a particular grid and the Tile refers to a selection
19+
of grid indices on that grid. The associated grid can be accessed using the ``.grid`` property.
20+
21+
.. Note ::
22+
23+
``nx`` and ``ny`` can be seen as 'right' and 'up', respectively, when the rotation of the grid is zero.
24+
If the grid is rotated, the tile rotates with it (naturally).
25+
This means that for a grid that is rotated 90 degrees,
26+
``nx`` refers to the number of cells up, and ``ny`` refers to the number of cells to the left.
27+
28+
..
29+
30+
Init parameters
31+
---------------
32+
grid: :class:`BaseGrid`
33+
The :class:`.TriGrid`, :class:`.RectGrid` or :class:`.HexGrid` the tile is associated with
34+
start_id: Union[Tuple[int, int], GridIndex]
35+
The starting cell of the Tile.
36+
The starting cell defines the bottom-left corner of the Tile if the associated grid is not rotated.
37+
nx: int
38+
The number of cells in x direction, starting from the ``start_id``
39+
ny: int
40+
The number of cells in y direction, starting from the ``start_id``
41+
42+
43+
"""
44+
45+
def __init__(
46+
self,
47+
grid: BaseGrid,
48+
start_id: Union[Tuple[int, int], GridIndex],
49+
nx: int,
50+
ny: int,
51+
):
52+
53+
if not numpy.isclose(nx % 1, 0):
54+
raise ValueError(f"Expected an integer for 'nx', got: {nx}")
55+
if nx < 1:
56+
raise ValueError(f"Expected 'nx' to be 1 or larger, got: {nx}")
57+
if not numpy.isclose(ny % 1, 0):
58+
raise ValueError(f"Expected an integer for 'ny', got: {ny}")
59+
if ny < 1:
60+
raise ValueError(f"Expected 'nx' to be 1 or larger, got: {ny}")
61+
if not len(GridIndex(start_id)) == 1:
62+
raise ValueError(
63+
"'start_id' must be a single pair of indices in the form (x,y), got: {start_id}"
64+
)
65+
start_id = (
66+
tuple(start_id.index)
67+
if isinstance(start_id, GridIndex)
68+
else tuple(start_id)
69+
)
70+
71+
if isinstance(grid, TriGrid):
72+
self._tile = PyO3TriTile(grid._grid, start_id, nx, ny)
73+
elif isinstance(grid, RectGrid):
74+
self._tile = PyO3RectTile(grid._grid, start_id, nx, ny)
75+
elif isinstance(grid, HexGrid):
76+
self._tile = PyO3HexTile(grid._grid, start_id, nx, ny)
77+
else:
78+
raise TypeError(
79+
f"Unexpected type for 'grid', expected a TriGrid, RectGrid or HexGrid, got a: {type(grid)}"
80+
)
81+
self.grid = grid.update()
82+
83+
@property
84+
def start_id(self):
85+
"""The starting cell of the Tile.
86+
The starting cell defines the bottom-left corner of the Tile if the associated grid is not rotated.
87+
"""
88+
return GridIndex(self._tile.start_id)
89+
90+
@property
91+
def nx(self):
92+
"""The number of cells in x direction, starting from the ``start_id``"""
93+
return self._tile.nx
94+
95+
@property
96+
def ny(self):
97+
"""The number of cells in y direction, starting from the ``start_id``"""
98+
return self._tile.ny
99+
100+
def corner_ids(self):
101+
"""The ids at the corners of the Tile
102+
103+
Returns
104+
-------
105+
:class:`.GridIndex`
106+
The :class:`.GridIndex` that contains the ids of the cells at
107+
the corners of the Tile in order: top-left, top-right, bottom-right, bottom-left
108+
(assuming the assicaited grid is not rotated)
109+
"""
110+
return GridIndex(self._tile.corner_ids())
111+
112+
def corners(self):
113+
"""The coordinates at the corners of the Tile
114+
115+
Returns
116+
-------
117+
`numpy.ndarray`
118+
A two-dimensional array that contais the x and y coordinates of
119+
the corners in order: top-left, top-right, bottom-right, bottom-left
120+
(assuming the assicaited grid is not rotated)
121+
"""
122+
return self._tile.corners()
123+
124+
@property
125+
def indices(self):
126+
"""The ids of all cells in the Tile.
127+
128+
Returns
129+
-------
130+
:class:`.GridIndex`
131+
The :class:`.GridIndex` that contains the indices in the Tile
132+
"""
133+
return GridIndex(self._tile.indices())
134+
135+
@property
136+
def bounds(self) -> Tuple[float, float, float, float]:
137+
"""The bounding box of the Tile in (xmin, ymin, xmax, ymax).
138+
If the associated grid is rotated, the this represents the bounding box
139+
that fully encapsulates the Tile and will contain more area than is
140+
covered by the rotated Tile.
141+
142+
Returns
143+
-------
144+
Tuple[float, float, float, float]
145+
The bounding box in (xmin, ymin, xmax, ymax)
146+
"""
147+
return self._tile.bounds()

gridkit/tri_grid.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from gridkit.base_grid import BaseGrid
88
from gridkit.bounded_grid import BoundedGrid
99
from gridkit.errors import AlignmentError, IntersectionError
10-
from gridkit.gridkit_rs import PyTriGrid
10+
from gridkit.gridkit_rs import PyO3TriGrid
1111
from gridkit.index import GridIndex, validate_index
1212

1313

@@ -81,7 +81,7 @@ def __init__(
8181
self._size = size
8282
self._radius = size / 3**0.5
8383
self._rotation = rotation
84-
self._grid = PyTriGrid(cellsize=size, offset=tuple(offset), rotation=rotation)
84+
self._grid = PyO3TriGrid(cellsize=size, offset=tuple(offset), rotation=rotation)
8585

8686
self.bounded_cls = BoundedTriGrid
8787
super(TriGrid, self).__init__(*args, **kwargs)
@@ -442,7 +442,7 @@ def _update_inner_grid(self, size=None, offset=None, rotation=None):
442442
offset = self.offset
443443
if rotation is None:
444444
rotation = self.rotation
445-
return PyTriGrid(cellsize=size, offset=offset, rotation=rotation)
445+
return PyO3TriGrid(cellsize=size, offset=offset, rotation=rotation)
446446

447447
def update(
448448
self, size=None, area=None, offset=None, rotation=None, crs=None, **kwargs

0 commit comments

Comments
 (0)