Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

h3shape_to_cells_experimental takes string containment modes #436

Merged
merged 14 commits into from
Jan 27, 2025
4 changes: 2 additions & 2 deletions .github/workflows/lint_and_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ jobs:

- name: Coverage Requirement - Library
run: |
pytest --cov=tests/test_lib --cov-fail-under=100
pytest tests/test_lib --cov=h3 --cov=tests/test_lib --cov-fail-under=100

- name: Coverage - Cython
run: |
pip install cython
cythonize tests/test_cython/cython_example.pyx
pytest --cov=tests/test_cython
pytest tests/test_cython --cov=tests/test_cython
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ avoid adding features or APIs which do not map onto the
## Unreleased

- Update to v4.2.0. (#432)
- Add `h3shape_to_cells_experimental` (#436)
- Add `polygon_to_cells_experimental` alias

## [4.1.2] - 2024-10-26

Expand Down
11 changes: 11 additions & 0 deletions docs/api_quick.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@ Note that this is reversed from [``__geo_interface__``](https://gist.github.com/
cells_to_geo
```

#### Additional functions

```{eval-rst}
.. currentmodule:: h3

.. autosummary::
polygon_to_cells
polygon_to_cells_experimental
h3shape_to_cells_experimental
```


## Specialized functions

Expand Down
4 changes: 2 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ purge: clear
-@rm -rf env

test:
./env/bin/pytest --cov=tests/test_lib --cov-fail-under=100
./env/bin/pytest tests/test_lib --cov=h3 --cov=tests/test_lib --cov-fail-under=100

./env/bin/pip install cython
./env/bin/cythonize tests/test_cython/cython_example.pyx
./env/bin/pytest --cov=tests/test_cython
./env/bin/pytest tests/test_cython --cov=tests/test_cython

lint:
./env/bin/ruff check
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ all = [
]

[tool.pytest.ini_options]
addopts = "--cov=h3 --cov-report=term-missing --durations=10"
addopts = "--cov-report=term-missing --durations=10"

[tool.coverage.run]
omit = [
Expand Down
14 changes: 7 additions & 7 deletions src/h3/_cy/latlng.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def polygons_to_cells(polygons, int res):
return hmm.to_mv()


def polygon_to_cells_experimental(outer, int res, int flags, holes=None):
def polygon_to_cells_experimental(outer, int res, int flag, holes=None):
""" Get the set of cells whose center is contained in a polygon.

The polygon is defined similarity to the GeoJson standard, with an exterior
Expand All @@ -221,8 +221,8 @@ def polygon_to_cells_experimental(outer, int res, int flags, holes=None):
A ring given by a sequence of lat/lng pairs.
res : int
The resolution of the output hexagons
flags : int
Polygon to cells flags, such as containment mode.
flag : int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem like an idiomatic change to me. While containment modes cannot be combined, I think the intention was to expand flags to cover other cases such as spherical vs Cartesian.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Cython function are internal and not part of the externally supported API, so we're free to change this in the future without officially breaking anything.

Polygon to cells flag, such as containment mode.
holes : list or tuple
A collection of rings, each given by a sequence of lat/lng pairs.
These describe any the "holes" in the polygon.
Expand All @@ -238,21 +238,21 @@ def polygon_to_cells_experimental(outer, int res, int flags, holes=None):
gp = GeoPolygon(outer, holes=holes)

check_for_error(
h3lib.maxPolygonToCellsSizeExperimental(&gp.gp, res, flags, &n)
h3lib.maxPolygonToCellsSizeExperimental(&gp.gp, res, flag, &n)
)

hmm = H3MemoryManager(n)
check_for_error(
h3lib.polygonToCellsExperimental(&gp.gp, res, flags, n, hmm.ptr)
h3lib.polygonToCellsExperimental(&gp.gp, res, flag, n, hmm.ptr)
)
mv = hmm.to_mv()

return mv


def polygons_to_cells_experimental(polygons, int res, int flags):
def polygons_to_cells_experimental(polygons, int res, int flag):
mvs = [
polygon_to_cells_experimental(outer=poly.outer, res=res, holes=poly.holes, flags=flags)
polygon_to_cells_experimental(outer=poly.outer, res=res, holes=poly.holes, flag=flag)
for poly in polygons
]

Expand Down
11 changes: 0 additions & 11 deletions src/h3/_h3shape.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
from abc import ABCMeta, abstractmethod
from enum import Enum


class ContainmentMode(int, Enum):
"""
Containment modes for use with ``polygon_to_cells_experimental``.
"""
containment_center = 0
containment_full = 1
containment_overlapping = 2
containment_overlapping_bbox = 3


class H3Shape(metaclass=ABCMeta):
Expand Down
82 changes: 50 additions & 32 deletions src/h3/api/basic_int/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# This file is **symlinked** across the APIs to ensure they are exactly the same.
from typing import Literal

from ... import _cy
from ..._h3shape import (
ContainmentMode,
H3Shape,
LatLngPoly,
LatLngMultiPoly,
Expand Down Expand Up @@ -468,6 +468,13 @@ def uncompact_cells(cells, res):
return _out_collection(hu)


def polygon_to_cells(h3shape, res):
"""
Alias for ``h3shape_to_cells``.
"""
return h3shape_to_cells(h3shape, res)


def h3shape_to_cells(h3shape, res):
"""
Return the collection of H3 cells at a given resolution whose center points
Expand Down Expand Up @@ -519,25 +526,46 @@ def h3shape_to_cells(h3shape, res):
return _out_collection(mv)


def polygon_to_cells(h3shape, res):
def polygon_to_cells_experimental(
h3shape: H3Shape,
res: int,
contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
):
"""
Alias for ``h3shape_to_cells``.
Alias for ``h3shape_to_cells_experimental``.
"""
return h3shape_to_cells(h3shape, res)
return h3shape_to_cells_experimental(h3shape, res, contain)


def h3shape_to_cells_experimental(h3shape, res, flags=0):
def h3shape_to_cells_experimental(
h3shape: H3Shape,
res: int,
contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
):
"""
Return the collection of H3 cells at a given resolution whose center points
are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``.
Experimental function similar to ``h3shape_to_cells``, but with support for
multiple cell containment modes.

Using ``contain='center'`` should give identical behavior as
``h3shape_to_cells``.

Note that this function is **experimental** and has no API stability gaurantees
across versions, so it may change in the future.


Parameters
----------
h3shape : ``H3Shape``
res : int
Resolution of the output cells
flags : ``ContainmentMode``, int, or string
Containment mode flags
contain : {'center', 'full', 'overlap', 'bbox_overlap'}, optional
Specifies the containment condition.
- 'center': Cell center is contained in shape
- 'full': Cell is fully contained in shape
- 'overlap': Cell is partially contained in shape
- 'bbox_overlap': Cell bounding box is partially contained in shape

Default is 'center'.

Returns
-------
Expand All @@ -550,7 +578,7 @@ def h3shape_to_cells_experimental(h3shape, res, flags=0):
... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
... (37.82, -122.54)],
... )
>>> h3.h3shape_to_cells_experimental(poly, 6, h3.ContainmentMode.containment_center)
>>> h3.h3shape_to_cells_experimental(poly, 6, 'center')
['862830807ffffff',
'862830827ffffff',
'86283082fffffff',
Expand All @@ -564,30 +592,27 @@ def h3shape_to_cells_experimental(h3shape, res, flags=0):
There is currently no guaranteed order of the output cells.
"""

if isinstance(flags, str):
try:
flags = ContainmentMode[flags]
except KeyError as e:
raise ValueError('Unrecognized flags: ' + flags) from e
if isinstance(flags, ContainmentMode):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would prefer to keep the enum option in addition to the stringly-typed form.

Would typing pick up the available string options and offer them as suggestions? I don't see them in type annotations right now.

Copy link
Contributor Author

@ajfriend ajfriend Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think typing in the future is the more natural way to handle this.

We don't currently have types, but they could be added. We don't use them in the area/length functions, for example, to specify the output units. Thus, the current form is a smaller change that's more consistent with the existing library design, and doesn't preclude adding typing or enums in the future.

Would typing pick up the available string options and offer them as suggestions? I don't see them in type annotations right now.

Yes, that's my understanding, if we were to add the type annotations.

Copy link
Contributor Author

@ajfriend ajfriend Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And actually, if you're interested in adding support for IDE tooling like autocomplete/suggestions, I think we should do that in a separate issue or PR. That's outside the scope of this PR (or #432), as it affects the library as a whole.

And I'd definitely welcome that discussion, as I think it is needed!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know what? The function is experimental, so let's just try it and see how it works. I do still think we should separately take a look at typing in the library holistically.

flags = int(flags)
if not isinstance(flags, int):
raise ValueError(
'Flags should be ContainmentMode, str, or int, but got: ' + str(type(flags))
)
contain_modes = {
'center': 0,
'full': 1,
'overlap': 2,
'bbox_overlap': 3,
}

flag = contain_modes[contain]

# todo: not sure if i want this dispatch logic here. maybe in the objects?
if isinstance(h3shape, LatLngPoly):
poly = h3shape
mv = _cy.polygon_to_cells_experimental(
poly.outer,
res=res,
holes=poly.holes,
flags=flags
res = res,
holes = poly.holes,
flag = flag,
)
elif isinstance(h3shape, LatLngMultiPoly):
mpoly = h3shape
mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flags=flags)
mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flag=flag)
elif isinstance(h3shape, H3Shape):
raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
else:
Expand All @@ -596,13 +621,6 @@ def h3shape_to_cells_experimental(h3shape, res, flags=0):
return _out_collection(mv)


def polygon_to_cells_experimental(h3shape, res, flags=0):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the name of the function in the C library and on the docs site. Why is the alias being removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we discussed this previously, and the preferred function name in h3-py is h3shape_to_cells since polygon doesn't account for multipolygons. We added the alias polygon_to_cells() in h3-py just as a pointer for folks to discover the preferred function.

As this function is experimental, and we already have the pointer, I think adding an additional alias is redundant. Also, we'll get back to the status quo when we merge the experimental function into the primary function.

"""
Alias for ``h3shape_to_cells_experimental``.
"""
return h3shape_to_cells_experimental(h3shape, res, flags=flags)


def cells_to_h3shape(cells, *, tight=True):
"""
Return an ``H3Shape`` describing the area covered by a collection of H3 cells.
Expand Down
Loading
Loading