From 259f31b6c861b06c8e843525c0a8b7df9f009bab Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Fri, 27 Sep 2024 15:25:56 -0700 Subject: [PATCH] cell_to_child_pos, child_pos_to_cell, cell_to_children_size (#405) * cell_to_child_pos and child_pos_to_cell * start exposing cell_to_children_size * checking resolutions * cell_to_children_size python function * adding tests from https://github.com/uber/h3-py/pull/404 * fiddling * fix test_cell_to_vertex; helps coverage by removing untouched branch * more tests * lint * exhaustive test for a few resolution pairs * lint * doc strings * error messages * stricter test * playing with argument order. example code in tests looks nicer * use parent_res instead of res * Use `res_*` instead of `*_res` * fix up changelog --- CHANGELOG.md | 5 +- docs/api_quick.md | 3 ++ src/h3/_cy/__init__.py | 3 ++ src/h3/_cy/cells.pxd | 3 ++ src/h3/_cy/cells.pyx | 48 +++++++++++++++--- src/h3/_cy/h3lib.pxd | 2 + src/h3/api/basic_int/__init__.py | 84 +++++++++++++++++++++++++++++--- tests/test_h3.py | 73 +++++++++++++++++++++++++-- 8 files changed, 202 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cbf64e..69d74874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,12 @@ avoid adding features or APIs which do not map onto the ## Unreleased -None. +- Add `cell_to_child_pos`, `child_pos_to_cell`, `cell_to_children_size` (#405) ## [4.1.0b1] - 2024-09-26 - Bump h3lib to v4.1.0 (#402) +- Add `polygon_to_cells` alias (#399) ## [4.0.0b7] - 2024-09-04 @@ -28,7 +29,7 @@ None. ## [4.0.0b6] - 2024-09-03 -- Added bindings for `cellToVertex`, `cellToVertexes`, `vertexToLatLng`, and `isValidVertex` (#323) +- Added bindings for `cellToVertex`, `cellToVertexes`, `vertexToLatLng`, and `isValidVertex` (#388) ## [4.0.0b5] - 2024-05-19 diff --git a/docs/api_quick.md b/docs/api_quick.md index a8c585c3..5503baef 100644 --- a/docs/api_quick.md +++ b/docs/api_quick.md @@ -79,6 +79,9 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. cell_to_parent cell_to_children cell_to_center_child + cell_to_children_size + cell_to_child_pos + child_pos_to_cell compact_cells uncompact_cells ``` diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index c739bc04..804b6b0e 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -22,7 +22,10 @@ grid_distance, grid_disk, grid_ring, + cell_to_children_size, cell_to_children, + cell_to_child_pos, + child_pos_to_cell, compact_cells, uncompact_cells, get_num_cells, diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index b6a36d18..06b35ee6 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -8,8 +8,11 @@ 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) cpdef H3int cell_to_parent(H3int h, res=*) except 0 +cpdef int64_t cell_to_children_size(H3int h, res=*) except -1 cpdef H3int[:] cell_to_children(H3int h, res=*) cpdef H3int cell_to_center_child(H3int h, res=*) except 0 +cpdef int64_t cell_to_child_pos(H3int child, int parent_res) except -1 +cpdef H3int child_pos_to_cell(H3int parent, int child_res, int64_t child_pos) except 0 cpdef H3int[:] compact_cells(const H3int[:] hu) cpdef H3int[:] uncompact_cells(const H3int[:] hc, int res) cpdef int64_t get_num_cells(int resolution) except -1 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 64be807e..2b57af27 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -3,7 +3,7 @@ from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes from .util cimport ( check_cell, - check_res, + check_res, # we don't use? check_distance, ) @@ -139,12 +139,12 @@ cpdef H3int[:] grid_ring(H3int h, int k): return mv + cpdef H3int cell_to_parent(H3int h, res=None) except 0: cdef: H3int parent - check_cell(h) # todo: do we want to check for validity here? or leave correctness to the user? - + check_cell(h) if res is None: res = get_resolution(h) - 1 @@ -157,12 +157,11 @@ cpdef H3int cell_to_parent(H3int h, res=None) except 0: return parent -cpdef H3int[:] cell_to_children(H3int h, res=None): +cpdef int64_t cell_to_children_size(H3int h, res=None) except -1: cdef: int64_t n check_cell(h) - if res is None: res = get_resolution(h) + 1 @@ -172,6 +171,16 @@ cpdef H3int[:] cell_to_children(H3int h, res=None): msg = msg.format(res, hex(h)) check_for_error_msg(err, msg) + return n + + +cpdef H3int[:] cell_to_children(H3int h, res=None): + check_cell(h) + if res is None: + res = get_resolution(h) + 1 + + n = cell_to_children_size(h, res) + hmm = H3MemoryManager(n) check_for_error( h3lib.cellToChildren(h, res, hmm.ptr) @@ -181,12 +190,12 @@ cpdef H3int[:] cell_to_children(H3int h, res=None): return mv + cpdef H3int cell_to_center_child(H3int h, res=None) except 0: cdef: H3int child check_cell(h) - if res is None: res = get_resolution(h) + 1 @@ -199,6 +208,33 @@ cpdef H3int cell_to_center_child(H3int h, res=None) except 0: return child +cpdef int64_t cell_to_child_pos(H3int child, int parent_res) except -1: + cdef: + int64_t child_pos + + check_cell(child) + err = h3lib.cellToChildPos(child, parent_res, &child_pos) + if err: + msg = "Couldn't find child pos of cell {} at res {}." + msg = msg.format(hex(child), parent_res) + check_for_error_msg(err, msg) + + return child_pos + + +cpdef H3int child_pos_to_cell(H3int parent, int child_res, int64_t child_pos) except 0: + cdef: + H3int child + + check_cell(parent) + err = h3lib.childPosToCell(child_pos, parent, child_res, &child) + if err: + msg = "Couldn't find child with pos {} at res {} from parent {}." + msg = msg.format(child_pos, child_res, hex(parent)) + check_for_error_msg(err, msg) + + return child + cpdef H3int[:] compact_cells(const H3int[:] hu): # todo: fix this with my own Cython object "wrapper" class? diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index eae1b853..a578a8fe 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -90,6 +90,8 @@ cdef extern from 'h3api.h': H3Error cellToParent( H3int h, int parentRes, H3int *parent) nogil H3Error cellToCenterChild(H3int h, int childRes, H3int *child) nogil + H3Error cellToChildPos(H3int child, int parentRes, int64_t *out) nogil + H3Error childPosToCell(int64_t childPos, H3int parent, int childRes, H3int *child) nogil H3Error cellToChildrenSize(H3int h, int childRes, int64_t *num) nogil # num/out/N? H3Error cellToChildren( H3int h, int childRes, H3int *children) nogil diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index d292ed69..60359662 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -185,7 +185,8 @@ def cell_to_latlng(h): lng : float Longitude """ - return _cy.cell_to_latlng(_in_scalar(h)) + h = _in_scalar(h) + return _cy.cell_to_latlng(h) def get_resolution(h): @@ -201,7 +202,8 @@ def get_resolution(h): int """ # todo: could also work for edges - return _cy.get_resolution(_in_scalar(h)) + h = _in_scalar(h) + return _cy.get_resolution(h) def cell_to_parent(h, res=None): @@ -266,7 +268,8 @@ def cell_to_boundary(h): ------- tuple of (lat, lng) tuples """ - return _cy.cell_to_boundary(_in_scalar(h)) + h = _in_scalar(h) + return _cy.cell_to_boundary(h) def grid_disk(h, k=1): @@ -288,7 +291,8 @@ def grid_disk(h, k=1): ----- There is currently no guaranteed order of the output cells. """ - mv = _cy.grid_disk(_in_scalar(h), k) + h = _in_scalar(h) + mv = _cy.grid_disk(h, k) return _out_collection(mv) @@ -312,11 +316,32 @@ def grid_ring(h, k=1): ----- There is currently no guaranteed order of the output cells. """ - mv = _cy.grid_ring(_in_scalar(h), k) + h = _in_scalar(h) + mv = _cy.grid_ring(h, k) return _out_collection(mv) +def cell_to_children_size(h, res=None): + """ + Number of children at resolution ``res`` of given cell. + + Parameters + ---------- + h : H3Cell + res : int or None, optional + The resolution for the children. + If ``None``, then ``res = resolution(h) + 1`` + + Returns + ------- + int + Count of children + """ + h = _in_scalar(h) + return _cy.cell_to_children_size(h, res) + + def cell_to_children(h, res=None): """ Children of a cell as an unordered collection. @@ -336,11 +361,58 @@ def cell_to_children(h, res=None): ----- There is currently no guaranteed order of the output cells. """ - mv = _cy.cell_to_children(_in_scalar(h), res) + h = _in_scalar(h) + mv = _cy.cell_to_children(h, res) return _out_collection(mv) +def cell_to_child_pos(child, res_parent): + """ + Child position index of given cell, with respect to its parent at ``res_parent``. + + The reverse operation can be done with ``child_pos_to_cell``. + + Parameters + ---------- + child : H3Cell + res_parent : int + + Returns + ------- + int + Integer index of the child with respect to parent cell. + """ + child = _in_scalar(child) + return _cy.cell_to_child_pos(child, res_parent) + + +def child_pos_to_cell(parent, res_child, child_pos): + """ + Get child H3 cell from a parent cell, child resolution, and child position index. + + The reverse operation can be done with ``cell_to_child_pos``. + + Parameters + ---------- + parent : H3Cell + res_child : int + Child cell resolution + child_pos : int + Integer position of child cell, releative to parent. + + + Returns + ------- + H3Cell + """ + parent = _in_scalar(parent) + child = _cy.child_pos_to_cell(parent, res_child, child_pos) + child = _out_scalar(child) + + return child + + # todo: nogil for expensive C operation? def compact_cells(cells): """ diff --git a/tests/test_h3.py b/tests/test_h3.py index 051480a6..760c3fab 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -400,12 +400,9 @@ def test_cell_to_vertex(): assert h3.cell_to_vertex('814c3ffffffffff', 2) == '2214c3ffffffffff' assert h3.cell_to_vertex('814c3ffffffffff', 3) == '2314c3ffffffffff' assert h3.cell_to_vertex('814c3ffffffffff', 4) == '2414c3ffffffffff' - try: + + with pytest.raises(h3.H3DomainError): h3.cell_to_vertex('814c3ffffffffff', 5) - except h3._cy.error_system.H3DomainError: - pass - else: - assert False # hexagon assert h3.cell_to_vertex('814d7ffffffffff', 0) == '2014d7ffffffffff' @@ -446,3 +443,69 @@ def test_is_valid_vertex(): assert h3.is_valid_vertex('2114c3ffffffffff') assert not h3.is_valid_vertex(2455495337847029759) assert not h3.is_valid_vertex('foobar') + + +def test_child_pos(): + h = '88283080ddfffff' + + assert h3.cell_to_child_pos(h, 8) == 0 + assert h3.cell_to_child_pos(h, 7) == 6 + assert h3.cell_to_child_pos(h, 6) == 41 + + with pytest.raises(h3.H3BaseException): + h3.cell_to_child_pos(h, 9) + + with pytest.raises(h3.H3BaseException): + h3.child_pos_to_cell(h, 9, -1) + + with pytest.raises(h3.H3BaseException): + h3.child_pos_to_cell(h, 9, 10000) + + +def test_child_pos2(): + h = '88283080ddfffff' + assert h == h3.child_pos_to_cell(h, 8, 0) + assert h == h3.child_pos_to_cell(h3.cell_to_parent(h, 7), 8, 6) + assert h == h3.child_pos_to_cell(h3.cell_to_parent(h, 6), 8, 41) + + +def test_cell_to_children_size(): + h = '8053fffffffffff' # hexagon + for r in range(16): + assert h3.cell_to_children_size(h, r) == 7**r + + +def test_cell_to_children_size2(): + cells = h3.get_res0_cells() + + for r in range(16): + total_cells = 120 * (7**r) + 2 + + assert total_cells == sum( + h3.cell_to_children_size(h, r) + for h in cells + ) + + +def test_child_pos3(): + def cells_at_res(res): + cells = h3.get_res0_cells() + for parent in cells: + yield from h3.cell_to_children(parent, res) + + def roundtrip(child, res_parent): + res_child = h3.get_resolution(child) + parent = h3.cell_to_parent(child, res_parent) + pos = h3.cell_to_child_pos(child, res_parent) + return h3.child_pos_to_cell(parent, res_child, pos) + + res_pairs = [ + (res_parent, res_child) + for res_parent in [0, 1] + for res_child in [0, 1, 2, 3] + if res_parent <= res_child + ] + + for res_parent, res_child in res_pairs: + for child in cells_at_res(res_child): + assert child == roundtrip(child, res_parent)