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.
Files changed (37) hide show
  1. h3_boundary-0.1.0/CMakeLists.txt +70 -0
  2. h3_boundary-0.1.0/LICENSE +21 -0
  3. h3_boundary-0.1.0/MANIFEST.in +9 -0
  4. h3_boundary-0.1.0/PKG-INFO +132 -0
  5. h3_boundary-0.1.0/README.md +96 -0
  6. h3_boundary-0.1.0/benchmarks/bench_cpp_binding.py +31 -0
  7. h3_boundary-0.1.0/benchmarks/bench_pure_cpp.cpp +36 -0
  8. h3_boundary-0.1.0/benchmarks/bench_pure_python.py +31 -0
  9. h3_boundary-0.1.0/benchmarks/benchmark_backends.py +93 -0
  10. h3_boundary-0.1.0/benchmarks/benchmark_cpp.cpp +40 -0
  11. h3_boundary-0.1.0/benchmarks/benchmark_python.py +36 -0
  12. h3_boundary-0.1.0/benchmarks/check_readme_snippets.py +55 -0
  13. h3_boundary-0.1.0/benchmarks/compare_algorithms.py +260 -0
  14. h3_boundary-0.1.0/benchmarks/compare_implementations.py +160 -0
  15. h3_boundary-0.1.0/benchmarks/generate_stats_table.py +62 -0
  16. h3_boundary-0.1.0/benchmarks/verify_boundary.py +56 -0
  17. h3_boundary-0.1.0/benchmarks/verify_cpp.cpp +34 -0
  18. h3_boundary-0.1.0/pyproject.toml +53 -0
  19. h3_boundary-0.1.0/setup.cfg +4 -0
  20. h3_boundary-0.1.0/setup.py +83 -0
  21. h3_boundary-0.1.0/src/bindings/python_bindings.cpp +230 -0
  22. h3_boundary-0.1.0/src/cpp/include/h3_toolkit.hpp +126 -0
  23. h3_boundary-0.1.0/src/cpp/src/h3_toolkit.cpp +887 -0
  24. h3_boundary-0.1.0/src/python/h3_boundary/__init__.py +379 -0
  25. h3_boundary-0.1.0/src/python/h3_boundary/geom.py +366 -0
  26. h3_boundary-0.1.0/src/python/h3_boundary/utils.py +703 -0
  27. h3_boundary-0.1.0/src/python/h3_boundary.egg-info/PKG-INFO +132 -0
  28. h3_boundary-0.1.0/src/python/h3_boundary.egg-info/SOURCES.txt +35 -0
  29. h3_boundary-0.1.0/src/python/h3_boundary.egg-info/dependency_links.txt +1 -0
  30. h3_boundary-0.1.0/src/python/h3_boundary.egg-info/requires.txt +9 -0
  31. h3_boundary-0.1.0/src/python/h3_boundary.egg-info/top_level.txt +1 -0
  32. h3_boundary-0.1.0/tests/cpp/test_h3_toolkit.cpp +51 -0
  33. h3_boundary-0.1.0/tests/python/test_buffering.py +109 -0
  34. h3_boundary-0.1.0/tests/python/test_bulk.py +172 -0
  35. h3_boundary-0.1.0/tests/python/test_indexing.py +145 -0
  36. h3_boundary-0.1.0/tests/python/test_parity.py +265 -0
  37. 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
+ [![PyPI](https://img.shields.io/pypi/v/h3-boundary.svg)](https://pypi.org/project/h3-boundary/)
42
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/h3-boundary.svg)](https://pypi.org/project/h3-boundary/)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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()