h3-boundary 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- h3_boundary-0.1.0/CMakeLists.txt +70 -0
- h3_boundary-0.1.0/LICENSE +21 -0
- h3_boundary-0.1.0/MANIFEST.in +9 -0
- h3_boundary-0.1.0/PKG-INFO +132 -0
- h3_boundary-0.1.0/README.md +96 -0
- h3_boundary-0.1.0/benchmarks/bench_cpp_binding.py +31 -0
- h3_boundary-0.1.0/benchmarks/bench_pure_cpp.cpp +36 -0
- h3_boundary-0.1.0/benchmarks/bench_pure_python.py +31 -0
- h3_boundary-0.1.0/benchmarks/benchmark_backends.py +93 -0
- h3_boundary-0.1.0/benchmarks/benchmark_cpp.cpp +40 -0
- h3_boundary-0.1.0/benchmarks/benchmark_python.py +36 -0
- h3_boundary-0.1.0/benchmarks/check_readme_snippets.py +55 -0
- h3_boundary-0.1.0/benchmarks/compare_algorithms.py +260 -0
- h3_boundary-0.1.0/benchmarks/compare_implementations.py +160 -0
- h3_boundary-0.1.0/benchmarks/generate_stats_table.py +62 -0
- h3_boundary-0.1.0/benchmarks/verify_boundary.py +56 -0
- h3_boundary-0.1.0/benchmarks/verify_cpp.cpp +34 -0
- h3_boundary-0.1.0/pyproject.toml +53 -0
- h3_boundary-0.1.0/setup.cfg +4 -0
- h3_boundary-0.1.0/setup.py +83 -0
- h3_boundary-0.1.0/src/bindings/python_bindings.cpp +230 -0
- h3_boundary-0.1.0/src/cpp/include/h3_toolkit.hpp +126 -0
- h3_boundary-0.1.0/src/cpp/src/h3_toolkit.cpp +887 -0
- h3_boundary-0.1.0/src/python/h3_boundary/__init__.py +379 -0
- h3_boundary-0.1.0/src/python/h3_boundary/geom.py +366 -0
- h3_boundary-0.1.0/src/python/h3_boundary/utils.py +703 -0
- h3_boundary-0.1.0/src/python/h3_boundary.egg-info/PKG-INFO +132 -0
- h3_boundary-0.1.0/src/python/h3_boundary.egg-info/SOURCES.txt +35 -0
- h3_boundary-0.1.0/src/python/h3_boundary.egg-info/dependency_links.txt +1 -0
- h3_boundary-0.1.0/src/python/h3_boundary.egg-info/requires.txt +9 -0
- h3_boundary-0.1.0/src/python/h3_boundary.egg-info/top_level.txt +1 -0
- h3_boundary-0.1.0/tests/cpp/test_h3_toolkit.cpp +51 -0
- h3_boundary-0.1.0/tests/python/test_buffering.py +109 -0
- h3_boundary-0.1.0/tests/python/test_bulk.py +172 -0
- h3_boundary-0.1.0/tests/python/test_indexing.py +145 -0
- h3_boundary-0.1.0/tests/python/test_parity.py +265 -0
- h3_boundary-0.1.0/tests/python/test_utils.py +47 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.14)
|
|
2
|
+
project(h3_toolkit)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
7
|
+
|
|
8
|
+
# Set by setup.py: builds only the Python extension (no tests/benchmarks).
|
|
9
|
+
option(H3_BOUNDARY_PIP_BUILD "Building via pip/setup.py" OFF)
|
|
10
|
+
|
|
11
|
+
include(FetchContent)
|
|
12
|
+
|
|
13
|
+
FetchContent_Declare(
|
|
14
|
+
h3
|
|
15
|
+
GIT_REPOSITORY https://github.com/uber/h3.git
|
|
16
|
+
GIT_TAG v4.1.0
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
FetchContent_MakeAvailable(h3)
|
|
20
|
+
|
|
21
|
+
# Boost.Geometry (header-only, used for polygon buffering)
|
|
22
|
+
find_package(Boost REQUIRED)
|
|
23
|
+
|
|
24
|
+
include_directories(${h3_SOURCE_DIR}/src/h3lib/include src/cpp/include)
|
|
25
|
+
|
|
26
|
+
add_library(h3_toolkit STATIC
|
|
27
|
+
src/cpp/src/h3_toolkit.cpp
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Link against h3 target (h3 usually exposes 'h3' target) and Boost
|
|
31
|
+
target_link_libraries(h3_toolkit PUBLIC h3 Boost::headers)
|
|
32
|
+
target_include_directories(h3_toolkit PUBLIC src/cpp/include ${Boost_INCLUDE_DIRS})
|
|
33
|
+
|
|
34
|
+
if(NOT H3_BOUNDARY_PIP_BUILD)
|
|
35
|
+
# Tests
|
|
36
|
+
enable_testing()
|
|
37
|
+
add_executable(h3_toolkit_test tests/cpp/test_h3_toolkit.cpp)
|
|
38
|
+
target_link_libraries(h3_toolkit_test h3_toolkit )
|
|
39
|
+
add_test(NAME h3_toolkit_test COMMAND h3_toolkit_test)
|
|
40
|
+
|
|
41
|
+
# Benchmark
|
|
42
|
+
add_executable(bench_pure_cpp benchmarks/bench_pure_cpp.cpp)
|
|
43
|
+
target_link_libraries(bench_pure_cpp h3_toolkit)
|
|
44
|
+
|
|
45
|
+
# Verification
|
|
46
|
+
add_executable(verify_cpp benchmarks/verify_cpp.cpp)
|
|
47
|
+
target_link_libraries(verify_cpp h3_toolkit)
|
|
48
|
+
endif()
|
|
49
|
+
|
|
50
|
+
# Python bindings: prefer the pybind11 from the build environment (pip build
|
|
51
|
+
# requirement passes -Dpybind11_DIR); fall back to FetchContent for bare
|
|
52
|
+
# CMake builds.
|
|
53
|
+
find_package(pybind11 CONFIG QUIET)
|
|
54
|
+
if(NOT pybind11_FOUND)
|
|
55
|
+
FetchContent_Declare(
|
|
56
|
+
pybind11
|
|
57
|
+
GIT_REPOSITORY https://github.com/pybind/pybind11.git
|
|
58
|
+
GIT_TAG v2.11.1
|
|
59
|
+
)
|
|
60
|
+
FetchContent_MakeAvailable(pybind11)
|
|
61
|
+
endif()
|
|
62
|
+
|
|
63
|
+
pybind11_add_module(_h3_boundary_cpp src/bindings/python_bindings.cpp)
|
|
64
|
+
target_link_libraries(_h3_boundary_cpp PRIVATE h3_toolkit h3)
|
|
65
|
+
target_include_directories(_h3_boundary_cpp PRIVATE src/cpp/include ${h3_SOURCE_DIR}/src/h3lib/include)
|
|
66
|
+
|
|
67
|
+
# Dev convenience: `cmake --install` drops the module into the source package
|
|
68
|
+
# (pip builds instead copy from the build dir into build_lib via setup.py).
|
|
69
|
+
install(TARGETS _h3_boundary_cpp DESTINATION ${CMAKE_SOURCE_DIR}/src/python/h3_boundary)
|
|
70
|
+
message(STATUS "Building Python bindings with pybind11")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 H3-Toolkit Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
include CMakeLists.txt
|
|
2
|
+
include LICENSE
|
|
3
|
+
include README.md
|
|
4
|
+
recursive-include src/cpp *.cpp *.hpp
|
|
5
|
+
recursive-include src/bindings *.cpp
|
|
6
|
+
recursive-include tests *.py *.cpp
|
|
7
|
+
recursive-include benchmarks *.py *.cpp
|
|
8
|
+
global-exclude *.so *.pyd *.dylib *.pyc
|
|
9
|
+
prune src/python/h3_boundary/__pycache__
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: h3-boundary
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: H3 cell boundary tracing and buffered polygons across resolution hierarchies, with optional C++ acceleration
|
|
5
|
+
Author: Kaveh Khoshkhah
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Khoshkhah/h3-boundary
|
|
8
|
+
Project-URL: Documentation, https://khoshkhah.github.io/h3-boundary/
|
|
9
|
+
Project-URL: Repository, https://github.com/Khoshkhah/h3-boundary
|
|
10
|
+
Project-URL: Issues, https://github.com/Khoshkhah/h3-boundary/issues
|
|
11
|
+
Keywords: h3,hexagon,geospatial,spatial-index,boundary,gis
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: C++
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: h3>=4.0.0
|
|
28
|
+
Requires-Dist: shapely>=2.0.0
|
|
29
|
+
Requires-Dist: geojson>=3.0.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
Requires-Dist: folium; extra == "dev"
|
|
34
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# H3-Boundary
|
|
38
|
+
|
|
39
|
+
**Boundary cells and boundary polygons for Uber's H3 grid.**
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/h3-boundary/)
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
43
|
+
[](https://opensource.org/licenses/MIT)
|
|
44
|
+
|
|
45
|
+
An H3 cell at one resolution contains thousands — or billions — of cells at finer resolutions. H3-Boundary works with the ones **on its edge**: it lists them, traces their outline, and builds polygons that safely contain them. Cost scales with the boundary, never with the interior.
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="https://raw.githubusercontent.com/Khoshkhah/h3-boundary/master/docs/assets/boundary_children.png"
|
|
49
|
+
alt="A resolution-5 H3 cell traced at resolution 8: its 78 boundary cells highlighted, its 265 interior cells greyed out"
|
|
50
|
+
width="720">
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center"><em>A resolution-5 cell traced at resolution 8: 78 boundary cells computed, 265 interior cells never generated.</em></p>
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install h3-boundary
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
Start from any H3 cell. Here we index downtown San Francisco at resolution 6 — a district-sized cell of about 36 km² — using `latlng_to_cell` from [h3-py](https://uber.github.io/h3-py/), which H3-Boundary already depends on.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import h3
|
|
65
|
+
import h3_boundary as h3b
|
|
66
|
+
|
|
67
|
+
cell = h3.latlng_to_cell(lat=37.7759, lng=-122.4180, res=6)
|
|
68
|
+
|
|
69
|
+
# 1. Its boundary cells: the descendants at resolution 10 (block-sized cells)
|
|
70
|
+
# that lie on its edge
|
|
71
|
+
edge = h3b.children_on_boundary_faces(cell, target_res=10)
|
|
72
|
+
len(edge) # 240, out of 2,401 descendants
|
|
73
|
+
|
|
74
|
+
# 2. Its exact outline — the shape those descendants actually fill
|
|
75
|
+
outline = h3b.cell_boundary_from_children(cell, target_res=10)
|
|
76
|
+
|
|
77
|
+
# 3. That same outline, grown by a safety margin
|
|
78
|
+
safe = h3b.get_buffered_boundary_polygon(cell, intermediate_res=10)
|
|
79
|
+
safe["properties"]["buffer_meters"] # 75.9 — the margin added
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Both polygons are GeoJSON Features, and both trace the boundary at resolution 10 — the difference is what they are for. The **outline** is the exact shape: draw it. On its own it is not safe to filter with, because cells finer than resolution 10 still poke slightly past it; **safe** pushes the edges out by one resolution-10 edge length (75.9 m), after which nothing can fall outside at *any* resolution. That is also why its parameter is called `intermediate_res` — there, tracing is only an intermediate step.
|
|
83
|
+
|
|
84
|
+
Why not just use the hexagon H3 draws for the cell? Because that hexagon is **not** where the descendants sit — they straddle it, as the figure above shows. Filtering fine-resolution data with it silently loses the cells along the edge (about 7% of them).
|
|
85
|
+
|
|
86
|
+
## Working at scale
|
|
87
|
+
|
|
88
|
+
Interiors explode; boundaries stay manageable. Boundary size has a closed form — `3**(depth + 1) - 3`, where `depth` is how many resolution levels you descend — so it is known before computing anything. A resolution-2 cell has close to two billion descendants at resolution 13, but only 531,438 of them lie on its boundary, and you never need to build even those to use them:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
res, target = 2, 13 # a country-sized cell, traced with ~44 m² cells
|
|
92
|
+
big = h3.latlng_to_cell(lat=37.7759, lng=-122.4180, res=res)
|
|
93
|
+
|
|
94
|
+
depth = target - res # 11 levels of subdivision between the two
|
|
95
|
+
total = 3 ** (depth + 1) - 3 # 531,438 boundary cells, known without counting
|
|
96
|
+
|
|
97
|
+
ids = h3b.boundary_cell_ids(big, target_res=target) # all of them, uint64, ~4 ms
|
|
98
|
+
mid = h3b.boundary_cell_at(big, target_res=target, n=total // 2) # any one, in microseconds
|
|
99
|
+
h3b.boundary_rank(big, mid) # the inverse — also a membership test
|
|
100
|
+
h3b.boundary_range(big, target_res=target, start=0, stop=100) # any slice — stream it, or shard it
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Disjoint slices reassemble into exactly the full boundary, so parallel workers need no coordination.
|
|
104
|
+
|
|
105
|
+
## Output formats
|
|
106
|
+
|
|
107
|
+
- **Cells**: hex strings (`children_on_boundary_faces`) or NumPy `uint64` arrays (`boundary_cell_ids`) — ready for h3-py, dataframes, or database joins.
|
|
108
|
+
- **Polygons**: GeoJSON Features — ready for folium, Leaflet, or PostGIS.
|
|
109
|
+
|
|
110
|
+
The package ships as a source distribution: it compiles a C++ extension during install when a toolchain is available (`cmake`, C++17, Boost headers) and falls back to pure Python otherwise. Same results either way, verified by a parity test suite.
|
|
111
|
+
|
|
112
|
+
## Documentation
|
|
113
|
+
|
|
114
|
+
**[khoshkhah.github.io/h3-boundary](https://khoshkhah.github.io/h3-boundary/)** — concepts, algorithm comparisons, benchmarks, and the full API.
|
|
115
|
+
|
|
116
|
+
Three runnable notebooks live in [`notebook/`](https://github.com/Khoshkhah/h3-boundary/tree/master/notebook): boundary tracing on a map, working with half-million-cell boundaries, and the buffering modes compared.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone https://github.com/Khoshkhah/h3-boundary.git
|
|
122
|
+
cd h3-boundary
|
|
123
|
+
conda env create -f environment.yml && conda activate h3-boundary
|
|
124
|
+
pip install -e .
|
|
125
|
+
pytest tests/python -v
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Contributions welcome — see [CONTRIBUTING.md](https://github.com/Khoshkhah/h3-boundary/blob/master/CONTRIBUTING.md).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT — see [LICENSE](https://github.com/Khoshkhah/h3-boundary/blob/master/LICENSE). Built on [Uber H3](https://h3geo.org/), [Boost.Geometry](https://www.boost.org/doc/libs/release/libs/geometry/) and [pybind11](https://github.com/pybind/pybind11).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# H3-Boundary
|
|
2
|
+
|
|
3
|
+
**Boundary cells and boundary polygons for Uber's H3 grid.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/h3-boundary/)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
An H3 cell at one resolution contains thousands — or billions — of cells at finer resolutions. H3-Boundary works with the ones **on its edge**: it lists them, traces their outline, and builds polygons that safely contain them. Cost scales with the boundary, never with the interior.
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://raw.githubusercontent.com/Khoshkhah/h3-boundary/master/docs/assets/boundary_children.png"
|
|
13
|
+
alt="A resolution-5 H3 cell traced at resolution 8: its 78 boundary cells highlighted, its 265 interior cells greyed out"
|
|
14
|
+
width="720">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center"><em>A resolution-5 cell traced at resolution 8: 78 boundary cells computed, 265 interior cells never generated.</em></p>
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install h3-boundary
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
Start from any H3 cell. Here we index downtown San Francisco at resolution 6 — a district-sized cell of about 36 km² — using `latlng_to_cell` from [h3-py](https://uber.github.io/h3-py/), which H3-Boundary already depends on.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import h3
|
|
29
|
+
import h3_boundary as h3b
|
|
30
|
+
|
|
31
|
+
cell = h3.latlng_to_cell(lat=37.7759, lng=-122.4180, res=6)
|
|
32
|
+
|
|
33
|
+
# 1. Its boundary cells: the descendants at resolution 10 (block-sized cells)
|
|
34
|
+
# that lie on its edge
|
|
35
|
+
edge = h3b.children_on_boundary_faces(cell, target_res=10)
|
|
36
|
+
len(edge) # 240, out of 2,401 descendants
|
|
37
|
+
|
|
38
|
+
# 2. Its exact outline — the shape those descendants actually fill
|
|
39
|
+
outline = h3b.cell_boundary_from_children(cell, target_res=10)
|
|
40
|
+
|
|
41
|
+
# 3. That same outline, grown by a safety margin
|
|
42
|
+
safe = h3b.get_buffered_boundary_polygon(cell, intermediate_res=10)
|
|
43
|
+
safe["properties"]["buffer_meters"] # 75.9 — the margin added
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Both polygons are GeoJSON Features, and both trace the boundary at resolution 10 — the difference is what they are for. The **outline** is the exact shape: draw it. On its own it is not safe to filter with, because cells finer than resolution 10 still poke slightly past it; **safe** pushes the edges out by one resolution-10 edge length (75.9 m), after which nothing can fall outside at *any* resolution. That is also why its parameter is called `intermediate_res` — there, tracing is only an intermediate step.
|
|
47
|
+
|
|
48
|
+
Why not just use the hexagon H3 draws for the cell? Because that hexagon is **not** where the descendants sit — they straddle it, as the figure above shows. Filtering fine-resolution data with it silently loses the cells along the edge (about 7% of them).
|
|
49
|
+
|
|
50
|
+
## Working at scale
|
|
51
|
+
|
|
52
|
+
Interiors explode; boundaries stay manageable. Boundary size has a closed form — `3**(depth + 1) - 3`, where `depth` is how many resolution levels you descend — so it is known before computing anything. A resolution-2 cell has close to two billion descendants at resolution 13, but only 531,438 of them lie on its boundary, and you never need to build even those to use them:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
res, target = 2, 13 # a country-sized cell, traced with ~44 m² cells
|
|
56
|
+
big = h3.latlng_to_cell(lat=37.7759, lng=-122.4180, res=res)
|
|
57
|
+
|
|
58
|
+
depth = target - res # 11 levels of subdivision between the two
|
|
59
|
+
total = 3 ** (depth + 1) - 3 # 531,438 boundary cells, known without counting
|
|
60
|
+
|
|
61
|
+
ids = h3b.boundary_cell_ids(big, target_res=target) # all of them, uint64, ~4 ms
|
|
62
|
+
mid = h3b.boundary_cell_at(big, target_res=target, n=total // 2) # any one, in microseconds
|
|
63
|
+
h3b.boundary_rank(big, mid) # the inverse — also a membership test
|
|
64
|
+
h3b.boundary_range(big, target_res=target, start=0, stop=100) # any slice — stream it, or shard it
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Disjoint slices reassemble into exactly the full boundary, so parallel workers need no coordination.
|
|
68
|
+
|
|
69
|
+
## Output formats
|
|
70
|
+
|
|
71
|
+
- **Cells**: hex strings (`children_on_boundary_faces`) or NumPy `uint64` arrays (`boundary_cell_ids`) — ready for h3-py, dataframes, or database joins.
|
|
72
|
+
- **Polygons**: GeoJSON Features — ready for folium, Leaflet, or PostGIS.
|
|
73
|
+
|
|
74
|
+
The package ships as a source distribution: it compiles a C++ extension during install when a toolchain is available (`cmake`, C++17, Boost headers) and falls back to pure Python otherwise. Same results either way, verified by a parity test suite.
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
**[khoshkhah.github.io/h3-boundary](https://khoshkhah.github.io/h3-boundary/)** — concepts, algorithm comparisons, benchmarks, and the full API.
|
|
79
|
+
|
|
80
|
+
Three runnable notebooks live in [`notebook/`](https://github.com/Khoshkhah/h3-boundary/tree/master/notebook): boundary tracing on a map, working with half-million-cell boundaries, and the buffering modes compared.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/Khoshkhah/h3-boundary.git
|
|
86
|
+
cd h3-boundary
|
|
87
|
+
conda env create -f environment.yml && conda activate h3-boundary
|
|
88
|
+
pip install -e .
|
|
89
|
+
pytest tests/python -v
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Contributions welcome — see [CONTRIBUTING.md](https://github.com/Khoshkhah/h3-boundary/blob/master/CONTRIBUTING.md).
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT — see [LICENSE](https://github.com/Khoshkhah/h3-boundary/blob/master/LICENSE). Built on [Uber H3](https://h3geo.org/), [Boost.Geometry](https://www.boost.org/doc/libs/release/libs/geometry/) and [pybind11](https://github.com/pybind/pybind11).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Benchmark: C++ bindings (pybind11)
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import h3
|
|
7
|
+
from h3_boundary._h3_boundary_cpp import children_on_boundary_faces
|
|
8
|
+
|
|
9
|
+
TARGET_RES = 15
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
print("=" * 50)
|
|
13
|
+
print("C++ BINDINGS (pybind11) Benchmark")
|
|
14
|
+
print("=" * 50)
|
|
15
|
+
|
|
16
|
+
cell_res0 = h3.get_res0_cells()[0]
|
|
17
|
+
print(f"Base cell: {cell_res0}")
|
|
18
|
+
print(f"Target resolution: {TARGET_RES}")
|
|
19
|
+
print()
|
|
20
|
+
print("Computing...")
|
|
21
|
+
|
|
22
|
+
start = time.perf_counter()
|
|
23
|
+
result = children_on_boundary_faces(cell_res0, TARGET_RES)
|
|
24
|
+
elapsed = time.perf_counter() - start
|
|
25
|
+
|
|
26
|
+
print(f"Count: {len(result):,}")
|
|
27
|
+
print(f"Time: {elapsed:.3f} seconds")
|
|
28
|
+
print(f"Rate: {len(result)/elapsed:,.0f} cells/sec")
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#include "h3_toolkit.hpp"
|
|
2
|
+
#include <h3api.h>
|
|
3
|
+
#include <iostream>
|
|
4
|
+
#include <chrono>
|
|
5
|
+
#include <set>
|
|
6
|
+
|
|
7
|
+
const int TARGET_RES = 15;
|
|
8
|
+
|
|
9
|
+
int main() {
|
|
10
|
+
std::cout << "==================================================" << std::endl;
|
|
11
|
+
std::cout << "PURE C++ Benchmark" << std::endl;
|
|
12
|
+
std::cout << "==================================================" << std::endl;
|
|
13
|
+
|
|
14
|
+
H3Index base_cells[122];
|
|
15
|
+
getRes0Cells(base_cells);
|
|
16
|
+
H3Index cell_res0 = base_cells[0];
|
|
17
|
+
|
|
18
|
+
std::cout << "Base cell: " << std::hex << cell_res0 << std::dec << std::endl;
|
|
19
|
+
std::cout << "Target resolution: " << TARGET_RES << std::endl;
|
|
20
|
+
std::cout << std::endl;
|
|
21
|
+
std::cout << "Computing..." << std::endl;
|
|
22
|
+
|
|
23
|
+
std::set<int> all_faces = {1, 2, 3, 4, 5, 6};
|
|
24
|
+
|
|
25
|
+
auto start = std::chrono::high_resolution_clock::now();
|
|
26
|
+
auto result = h3_toolkit::children_on_boundary_faces(cell_res0, TARGET_RES, all_faces);
|
|
27
|
+
auto end = std::chrono::high_resolution_clock::now();
|
|
28
|
+
|
|
29
|
+
std::chrono::duration<double> elapsed = end - start;
|
|
30
|
+
|
|
31
|
+
std::cout << "Count: " << result.size() << std::endl;
|
|
32
|
+
std::cout << "Time: " << elapsed.count() << " seconds" << std::endl;
|
|
33
|
+
std::cout << "Rate: " << (result.size() / elapsed.count()) << " cells/sec" << std::endl;
|
|
34
|
+
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Benchmark: Pure Python implementation
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import h3
|
|
7
|
+
from h3_boundary.utils import children_on_boundary_faces
|
|
8
|
+
|
|
9
|
+
TARGET_RES = 15
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
print("=" * 50)
|
|
13
|
+
print("PURE PYTHON Benchmark")
|
|
14
|
+
print("=" * 50)
|
|
15
|
+
|
|
16
|
+
cell_res0 = h3.get_res0_cells()[0]
|
|
17
|
+
print(f"Base cell: {cell_res0}")
|
|
18
|
+
print(f"Target resolution: {TARGET_RES}")
|
|
19
|
+
print()
|
|
20
|
+
print("Computing...")
|
|
21
|
+
|
|
22
|
+
start = time.perf_counter()
|
|
23
|
+
result = children_on_boundary_faces(cell_res0, TARGET_RES)
|
|
24
|
+
elapsed = time.perf_counter() - start
|
|
25
|
+
|
|
26
|
+
print(f"Count: {len(result):,}")
|
|
27
|
+
print(f"Time: {elapsed:.3f} seconds")
|
|
28
|
+
print(f"Rate: {len(result)/elapsed:,.0f} cells/sec")
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Benchmark comparing Python vs C++ backends for children_on_boundary_faces
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import h3
|
|
7
|
+
|
|
8
|
+
# Configuration
|
|
9
|
+
TARGET_RES = 15 # Change this to test different resolutions
|
|
10
|
+
|
|
11
|
+
def benchmark_python(cell_res0, target_res):
|
|
12
|
+
"""Run with pure Python backend"""
|
|
13
|
+
from h3_boundary.utils import children_on_boundary_faces as py_children
|
|
14
|
+
|
|
15
|
+
start = time.perf_counter()
|
|
16
|
+
result = py_children(cell_res0, target_res)
|
|
17
|
+
elapsed = time.perf_counter() - start
|
|
18
|
+
|
|
19
|
+
return result, elapsed
|
|
20
|
+
|
|
21
|
+
def benchmark_cpp(cell_res0, target_res):
|
|
22
|
+
"""Run with C++ backend"""
|
|
23
|
+
try:
|
|
24
|
+
from h3_boundary._h3_boundary_cpp import children_on_boundary_faces as cpp_children
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
return None, None, str(e)
|
|
27
|
+
|
|
28
|
+
start = time.perf_counter()
|
|
29
|
+
result = cpp_children(cell_res0, target_res)
|
|
30
|
+
elapsed = time.perf_counter() - start
|
|
31
|
+
|
|
32
|
+
return result, elapsed, None
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
print("=" * 60)
|
|
36
|
+
print("H3-Boundary Backend Benchmark")
|
|
37
|
+
print("=" * 60)
|
|
38
|
+
print()
|
|
39
|
+
|
|
40
|
+
cell_res0 = h3.get_res0_cells()[0]
|
|
41
|
+
print(f"Base cell: {cell_res0}")
|
|
42
|
+
print(f"Target resolution: {TARGET_RES}")
|
|
43
|
+
print()
|
|
44
|
+
|
|
45
|
+
# Python benchmark
|
|
46
|
+
print("Running Python backend...")
|
|
47
|
+
py_result, py_time = benchmark_python(cell_res0, TARGET_RES)
|
|
48
|
+
py_count = len(py_result)
|
|
49
|
+
print(f" Count: {py_count:,}")
|
|
50
|
+
print(f" Time: {py_time:.3f}s")
|
|
51
|
+
if py_time > 0:
|
|
52
|
+
print(f" Rate: {py_count/py_time:,.0f} cells/sec")
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
# C++ benchmark
|
|
56
|
+
print("Running C++ backend...")
|
|
57
|
+
cpp_result, cpp_time, error = benchmark_cpp(cell_res0, TARGET_RES)
|
|
58
|
+
|
|
59
|
+
if error:
|
|
60
|
+
print(f" ERROR: {error}")
|
|
61
|
+
print()
|
|
62
|
+
print("To build C++ bindings:")
|
|
63
|
+
print(" cd ~/projects/h3-toolkit")
|
|
64
|
+
print(" rm -rf build && mkdir build && cd build")
|
|
65
|
+
print(" cmake .. && make _h3_boundary_cpp")
|
|
66
|
+
print(" cp _h3_boundary_cpp*.so ../src/python/h3_boundary/")
|
|
67
|
+
else:
|
|
68
|
+
cpp_count = len(cpp_result)
|
|
69
|
+
print(f" Count: {cpp_count:,}")
|
|
70
|
+
print(f" Time: {cpp_time:.3f}s")
|
|
71
|
+
if cpp_time > 0:
|
|
72
|
+
print(f" Rate: {cpp_count/cpp_time:,.0f} cells/sec")
|
|
73
|
+
print()
|
|
74
|
+
|
|
75
|
+
# Comparison
|
|
76
|
+
print("-" * 60)
|
|
77
|
+
if cpp_time > 0 and py_time > 0:
|
|
78
|
+
speedup = py_time / cpp_time
|
|
79
|
+
print(f"C++ is {speedup:.1f}x faster than Python")
|
|
80
|
+
|
|
81
|
+
results_match = py_count == cpp_count
|
|
82
|
+
print(f"Results match: {results_match}")
|
|
83
|
+
|
|
84
|
+
if not results_match:
|
|
85
|
+
print()
|
|
86
|
+
print("DEBUG: Checking first 5 results...")
|
|
87
|
+
py_sorted = sorted(py_result)[:5]
|
|
88
|
+
cpp_sorted = sorted(cpp_result)[:5]
|
|
89
|
+
print(f" Python: {py_sorted}")
|
|
90
|
+
print(f" C++: {cpp_sorted}")
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#include "h3_toolkit.hpp"
|
|
2
|
+
#include <h3api.h>
|
|
3
|
+
#include <iostream>
|
|
4
|
+
#include <chrono>
|
|
5
|
+
#include <set>
|
|
6
|
+
|
|
7
|
+
int main() {
|
|
8
|
+
// Get a resolution 0 cell (base cell)
|
|
9
|
+
// Get first base cell
|
|
10
|
+
int num_base_cells = 122;
|
|
11
|
+
H3Index base_cells[122];
|
|
12
|
+
getRes0Cells(base_cells);
|
|
13
|
+
|
|
14
|
+
H3Index cell_res0 = base_cells[0];
|
|
15
|
+
|
|
16
|
+
std::cout << "Base cell: " << std::hex << cell_res0 << std::dec << std::endl;
|
|
17
|
+
std::cout << "Resolution: " << getResolution(cell_res0) << std::endl;
|
|
18
|
+
std::cout << std::endl;
|
|
19
|
+
|
|
20
|
+
int target_res = 15;
|
|
21
|
+
|
|
22
|
+
std::cout << "Computing boundary children from res 0 to res " << target_res << "..." << std::endl;
|
|
23
|
+
std::cout << "This may take a while..." << std::endl;
|
|
24
|
+
std::cout << std::endl;
|
|
25
|
+
|
|
26
|
+
auto start = std::chrono::high_resolution_clock::now();
|
|
27
|
+
|
|
28
|
+
std::set<int> all_faces = {1, 2, 3, 4, 5, 6};
|
|
29
|
+
auto boundary_children = h3_toolkit::children_on_boundary_faces(cell_res0, target_res, all_faces);
|
|
30
|
+
|
|
31
|
+
auto end = std::chrono::high_resolution_clock::now();
|
|
32
|
+
std::chrono::duration<double> elapsed = end - start;
|
|
33
|
+
|
|
34
|
+
std::cout << "Results:" << std::endl;
|
|
35
|
+
std::cout << " Number of boundary children: " << boundary_children.size() << std::endl;
|
|
36
|
+
std::cout << " Time elapsed: " << elapsed.count() << " seconds" << std::endl;
|
|
37
|
+
std::cout << " Rate: " << (boundary_children.size() / elapsed.count()) << " cells/second" << std::endl;
|
|
38
|
+
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Benchmark: children_on_boundary_faces from res 0 to res 15
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import h3
|
|
7
|
+
from h3_boundary.utils import children_on_boundary_faces
|
|
8
|
+
|
|
9
|
+
def benchmark():
|
|
10
|
+
# Get a resolution 0 cell (base cell)
|
|
11
|
+
# There are 122 base cells, pick one
|
|
12
|
+
cell_res0 = h3.get_res0_cells()[0]
|
|
13
|
+
print(f"Base cell: {cell_res0}")
|
|
14
|
+
print(f"Resolution: {h3.get_resolution(cell_res0)}")
|
|
15
|
+
print()
|
|
16
|
+
|
|
17
|
+
target_res = 15
|
|
18
|
+
|
|
19
|
+
print(f"Computing boundary children from res 0 to res {target_res}...")
|
|
20
|
+
print("This may take a while...")
|
|
21
|
+
print()
|
|
22
|
+
|
|
23
|
+
start_time = time.perf_counter()
|
|
24
|
+
|
|
25
|
+
boundary_children = children_on_boundary_faces(cell_res0, target_res)
|
|
26
|
+
|
|
27
|
+
end_time = time.perf_counter()
|
|
28
|
+
elapsed = end_time - start_time
|
|
29
|
+
|
|
30
|
+
print(f"Results:")
|
|
31
|
+
print(f" Number of boundary children: {len(boundary_children):,}")
|
|
32
|
+
print(f" Time elapsed: {elapsed:.3f} seconds")
|
|
33
|
+
print(f" Rate: {len(boundary_children) / elapsed:,.0f} cells/second")
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
benchmark()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Executes every ```python block in README.md and the docs, in order, so a
|
|
4
|
+
snippet cannot ship broken or reference an undefined name.
|
|
5
|
+
|
|
6
|
+
Blocks in one file share a namespace, the way a reader reading top to bottom
|
|
7
|
+
would experience them. Lines that are obviously illustrative rather than
|
|
8
|
+
runnable (a bare `...`) are skipped.
|
|
9
|
+
|
|
10
|
+
PYTHONPATH=src/python python benchmarks/check_readme_snippets.py
|
|
11
|
+
"""
|
|
12
|
+
import pathlib
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import traceback
|
|
16
|
+
|
|
17
|
+
FILES = ["README.md", "docs/index.md"]
|
|
18
|
+
BLOCK = re.compile(r"```python\n(.*?)```", re.DOTALL)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check(path):
|
|
22
|
+
text = pathlib.Path(path).read_text()
|
|
23
|
+
blocks = BLOCK.findall(text)
|
|
24
|
+
if not blocks:
|
|
25
|
+
print(f" {path}: no python blocks")
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
ns = {}
|
|
29
|
+
failures = 0
|
|
30
|
+
for i, block in enumerate(blocks, 1):
|
|
31
|
+
if block.strip() in ("", "..."):
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
exec(compile(block, f"{path}#block{i}", "exec"), ns)
|
|
35
|
+
except Exception:
|
|
36
|
+
failures += 1
|
|
37
|
+
print(f" {path} block {i} FAILED:")
|
|
38
|
+
print(" " + "\n ".join(block.strip().splitlines()))
|
|
39
|
+
print(" " + traceback.format_exc().strip().replace("\n", "\n "))
|
|
40
|
+
status = "ok" if not failures else f"{failures} FAILED"
|
|
41
|
+
print(f" {path}: {len(blocks)} blocks — {status}")
|
|
42
|
+
return failures
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main():
|
|
46
|
+
print("checking documentation snippets")
|
|
47
|
+
failures = sum(check(p) for p in FILES)
|
|
48
|
+
if failures:
|
|
49
|
+
print(f"\n{failures} snippet(s) failed")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
print("\nall snippets run")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|