Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ h3c/
.cache/
h3/out/
.ruff_cache/
_build/

# Generated C code from Cython test
tests/**/*.c
Expand Down
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ avoid adding features or APIs which do not map onto the

## Unreleased

None.
- Update `h3lib` to v4.4.1. (#470)
- Add new error codes:
- `H3IndexInvalidError`
- `H3BaseCellDomainError`
- `H3DigitDomainError`
- `H3DeletedDigitError`
- Add new functions:
- `is_valid_index`
- `get_index_digit`
- `construct_cell`
- `deconstruct_cell`

## [4.3.1] - 2025-08-10

Expand Down
4 changes: 4 additions & 0 deletions docs/api_quick.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and should be generally aligned with the
is_res_class_III
is_valid_directed_edge
is_valid_vertex
is_valid_index
```

## Index representation
Expand All @@ -49,6 +50,9 @@ and should be generally aligned with the
get_num_cells
get_resolution
get_base_cell_number
get_index_digit
construct_cell
deconstruct_cell
```

## Geographic coordinates
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = 'scikit_build_core.build'

[project]
name = 'h3'
version = '4.3.1'
version = '4.4.0'
description = "Uber's hierarchical hexagonal geospatial indexing system"
readme = 'readme.md'
license = {file = 'LICENSE'}
Expand Down Expand Up @@ -50,7 +50,7 @@ numpy = ['numpy']
test = ['pytest', 'pytest-cov', 'ruff', 'numpy']
all = [
'h3[test]',
'jupyter-book',
'jupyter-book<2',
'sphinx>=7.3.3', # https://github.com/sphinx-doc/sphinx/issues/12290
'jupyterlab',
'jupyterlab-geojson',
Expand Down
4 changes: 4 additions & 0 deletions src/h3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@
H3MemoryAllocError,
H3MemoryBoundsError,
H3OptionInvalidError,
H3IndexInvalidError,
H3BaseCellDomainError,
H3DigitDomainError,
H3DeletedDigitError,
)
10 changes: 10 additions & 0 deletions src/h3/_cy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
"""

from .cells import (
is_valid_index,
is_valid_cell,
is_pentagon,
get_base_cell_number,
get_resolution,
get_index_digit,
construct_cell,
cell_to_parent,
grid_distance,
grid_disk,
Expand Down Expand Up @@ -109,4 +112,11 @@
H3MemoryAllocError,
H3MemoryBoundsError,
H3OptionInvalidError,
H3IndexInvalidError,
H3BaseCellDomainError,
H3DigitDomainError,
H3DeletedDigitError,

get_H3_ERROR_END,
error_code_to_exception,
)
3 changes: 3 additions & 0 deletions src/h3/_cy/cells.pxd
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from .h3lib cimport bool, int64_t, H3int

cpdef bool is_valid_index(H3int h)
cpdef bool is_valid_cell(H3int h)
cpdef bool is_pentagon(H3int h)
cpdef int get_base_cell_number(H3int h) except -1
cpdef int get_resolution(H3int h) except -1
cpdef int get_index_digit(H3int h, int res) except -1
cpdef H3int construct_cell(int baseCellNumber, const int[:] digits) except 0
cpdef int grid_distance(H3int h1, H3int h2) except -1
cpdef H3int[:] grid_disk(H3int h, int k)
cpdef H3int[:] grid_ring(H3int h, int k)
Expand Down
38 changes: 37 additions & 1 deletion src/h3/_cy/cells.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes

from .util cimport (
check_cell,
check_res, # we don't use?
check_index,
check_distance,
)

from .error_system cimport (
H3Error,
check_for_error,
check_for_error_msg,
)
Expand All @@ -19,6 +20,14 @@ from .memory cimport (

# todo: add notes about Cython exception handling

cpdef bool is_valid_index(H3int h):
"""Validates an H3 index (cell, vertex, or directed edge).

Returns
-------
boolean
"""
return h3lib.isValidIndex(h) == 1

# bool is a python type, so we don't need the except clause
cpdef bool is_valid_cell(H3int h):
Expand Down Expand Up @@ -49,6 +58,33 @@ cpdef int get_resolution(H3int h) except -1:

return h3lib.getResolution(h)

cpdef int get_index_digit(H3int h, int res) except -1:
cdef:
int digit

check_index(h)

check_for_error(
h3lib.getIndexDigit(h, res, &digit)
)

return digit

cpdef H3int construct_cell(int base_cell_number, const int[:] digits) except 0:
cdef:
H3int out
int res = len(digits)
H3Error err

if res > 0:
err = h3lib.constructCell(res, base_cell_number, &digits[0], &out)
else:
err = h3lib.constructCell(res, base_cell_number, NULL, &out)

check_for_error(err)

return out


cpdef int grid_distance(H3int h1, H3int h2) except -1:
""" Compute the grid distance between two cells
Expand Down
3 changes: 3 additions & 0 deletions src/h3/_cy/error_system.pxd
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .h3lib cimport H3Error

cpdef error_code_to_exception(H3Error err)
cdef check_for_error(H3Error err)
cdef check_for_error_msg(H3Error err, str msg)
cpdef H3Error get_H3_ERROR_END()
37 changes: 33 additions & 4 deletions src/h3/_cy/error_system.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ Summarizing, all exceptions originating from the C library inherit from
- H3MemoryAllocError
- H3MemoryBoundsError
- H3OptionInvalidError
- H3IndexInvalidError
- H3BaseCellDomainError
- H3DigitDomainError
- H3DeletedDigitError


# TODO: add tests verifying that concrete exception classes have the right error codes associated with them
Expand Down Expand Up @@ -87,6 +91,11 @@ from .h3lib cimport (
E_MEMORY_ALLOC,
E_MEMORY_BOUNDS,
E_OPTION_INVALID,
E_INDEX_INVALID,
E_BASE_CELL_DOMAIN,
E_DIGIT_DOMAIN,
E_DELETED_DIGIT,
H3_ERROR_END # sentinel value
)

@contextmanager
Expand Down Expand Up @@ -169,6 +178,10 @@ with _the_error(H3ValueError) as e:
class H3NotNeighborsError(e): ...
class H3ResMismatchError(e): ...
class H3OptionInvalidError(e): ...
class H3IndexInvalidError(e): ...
class H3BaseCellDomainError(e): ...
class H3DigitDomainError(e): ...
class H3DeletedDigitError(e): ...


"""
Expand All @@ -192,6 +205,10 @@ error_mapping = {
E_MEMORY_ALLOC: H3MemoryAllocError,
E_MEMORY_BOUNDS: H3MemoryBoundsError,
E_OPTION_INVALID: H3OptionInvalidError,
E_INDEX_INVALID: H3IndexInvalidError,
E_BASE_CELL_DOMAIN: H3BaseCellDomainError,
E_DIGIT_DOMAIN: H3DigitDomainError,
E_DELETED_DIGIT: H3DeletedDigitError,
}

# Go back and modify the class definitions so that each concrete exception
Expand All @@ -207,25 +224,37 @@ for code, ex in error_mapping.items():
# TODO: Move the helpers to util?
# TODO: Unclear how/where to expose these functions. cdef/cpdef?

cdef code_to_exception(H3Error err):
cpdef error_code_to_exception(H3Error err):
"""
Return Python exception corresponding to integer error code
given via the H3ErrorCodes enum in `h3api.h.in` in the C library.
"""
if err == E_SUCCESS:
return None
elif err in error_mapping:
return error_mapping[err]
else:
raise UnknownH3ErrorCode(err)
return UnknownH3ErrorCode(err)

cdef check_for_error(H3Error err):
ex = code_to_exception(err)
ex = error_code_to_exception(err)
if ex:
raise ex

cpdef H3Error get_H3_ERROR_END():
"""
Return integer H3_ERROR_END from the H3ErrorCodes enum
in `h3api.h.in` in the C library, which is one greater than
the last valid error code.
"""
return H3_ERROR_END

# todo: There's no easy way to do `*args` in `cdef` functions, but I'm also
# not sure this even needs to be a Cython `cdef` function at all, or that
# any of the other helper functions need to be in Cython.
# todo: Revisit after we've played with this a bit.
# todo: also: maybe the extra messages aren't that much more helpful...
cdef check_for_error_msg(H3Error err, str msg):
ex = code_to_exception(err)
ex = error_code_to_exception(err)
if ex:
raise ex(msg)
8 changes: 8 additions & 0 deletions src/h3/_cy/h3lib.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ cdef extern from 'h3api.h':
E_MEMORY_ALLOC = 13
E_MEMORY_BOUNDS = 14
E_OPTION_INVALID = 15
E_INDEX_INVALID = 16
E_BASE_CELL_DOMAIN = 17
E_DIGIT_DOMAIN = 18
E_DELETED_DIGIT = 19
H3_ERROR_END # sentinel value

ctypedef struct LatLng:
double lat # in radians
Expand Down Expand Up @@ -71,12 +76,15 @@ cdef extern from 'h3api.h':
int isResClassIII(H3int h) nogil
int isValidDirectedEdge(H3int edge) nogil
int isValidVertex(H3int v) nogil
int isValidIndex(H3int h) nogil

double degsToRads(double degrees) nogil
double radsToDegs(double radians) nogil

int getResolution(H3int h) nogil
int getBaseCellNumber(H3int h) nogil
H3Error getIndexDigit(H3int h, int res, int *out) nogil
H3Error constructCell(int res, int baseCellNumber, const int *digits, H3int *out) nogil

H3Error latLngToCell(const LatLng *g, int res, H3int *out) nogil
H3Error cellToLatLng(H3int h, LatLng *) nogil
Expand Down
3 changes: 2 additions & 1 deletion src/h3/_cy/util.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cpdef H3str int_to_str(H3int x)

cdef check_cell(H3int h)
cdef check_edge(H3int e)
cdef check_res(int res)
cdef check_vertex(H3int v)
cdef check_index(H3int h)
cdef check_res(int res)
cdef check_distance(int k)
16 changes: 14 additions & 2 deletions src/h3/_cy/util.pyx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge, isValidVertex
from .h3lib cimport (
H3int,
H3str,
isValidCell,
isValidDirectedEdge,
isValidVertex,
isValidIndex,
)

cimport h3lib

Expand All @@ -7,7 +14,8 @@ from .error_system import (
H3DomainError,
H3DirEdgeInvalidError,
H3CellInvalidError,
H3VertexInvalidError
H3VertexInvalidError,
H3IndexInvalidError,
)

cdef h3lib.LatLng deg2coord(double lat, double lng) nogil:
Expand Down Expand Up @@ -78,6 +86,10 @@ cdef check_vertex(H3int v):
if isValidVertex(v) == 0:
raise H3VertexInvalidError('Integer is not a valid H3 vertex: {}'.format(hex(v)))

cdef check_index(H3int h):
if isValidIndex(h) == 0:
raise H3IndexInvalidError('Integer is not a valid H3 index: {}'.format(hex(h)))

cdef check_res(int res):
if (res < 0) or (res > 15):
raise H3ResDomainError(res)
Expand Down
Loading
Loading