Source code for gedih3.h3utils

# Copyright (C) 2026, University of Maryland. All Rights Reserved.
# Authors: Tiago de Conto, Amelia Grace Holcomb
# For commercial licensing inquiries, contact UM Ventures at umdtechtransfer@umd.edu

import h3

[docs] def get_all_h3_hexagons(resolution: int): """Get all H3 hexagons at a given resolution level""" if resolution == 0: return list(h3.get_res0_cells()) # For higher resolutions, start with res 0 and get all children all_hexagons = [] base_cells = h3.get_res0_cells() for base_cell in base_cells: children = h3.cell_to_children(base_cell, resolution) all_hexagons.extend(children) return all_hexagons
[docs] def iter_all_h3_hexagons(resolution: int): """Memory-efficient iterator for all H3 hexagons at resolution""" base_cells = h3.get_res0_cells() for base_cell in base_cells: children = h3.cell_to_children(base_cell, resolution) for child in children: yield child
[docs] def fix_h3_geometry(hex:str): """Using the antimeridian package for robust handling.""" from shapely.geometry import Polygon from antimeridian import fix_polygon boundary_coords = h3.cell_to_boundary(hex) polygon = Polygon([(lon, lat) for lat, lon in boundary_coords]) # Fix antimeridian crossing fixed_geometry = fix_polygon(polygon) return fixed_geometry
[docs] def h3_expand_ring(cells, ring=1, valid=None): """Expand a set of H3 cells by their ``grid_disk`` neighbors. The shared overhang-safety primitive: shots are stored under their hierarchy partition, which can overhang the partition's polygon by ~0.18 x edge length (see ``_H3_OVERHANG_FRACTION`` in utils.py) — far less than one cell width, so any cell set selected by exact polygon intersection only needs its ring-1 neighbors to cover all storage partitions. Used by ``intersect_h3_geometries``, ``egi_h3_intersection`` and ``geoseries_to_filter``. Parameters ---------- cells : iterable of str H3 cell IDs (all at the same resolution). ring : int, default 1 ``grid_disk`` radius. valid : set, optional When given, only neighbors in this set are added (e.g. the partitions that actually exist in a database). Returns ------- list[str] Sorted union of ``cells`` and their (filtered) neighbors. """ expanded = set(cells) for cell in cells: for nbr in h3.grid_disk(cell, ring): if valid is None or nbr in valid: expanded.add(nbr) return sorted(expanded)
[docs] def intersect_h3_geometries(spatial, res=3, h3_ids=None, expand_ring=1): """Return H3 cells whose data footprint may intersect ``spatial``. H3 children are not geometrically contained in their parents: a shot stored under partition ``cell_to_parent(latlng_to_cell(lon, lat, index_res), part_res)`` can sit up to ~0.14-0.16 x the parent's edge length outside the parent's own polygon (~8-10 km at partition level 3; see ``_H3_OVERHANG_FRACTION`` in utils.py). A ROI touching that overhang band intersects the polygon the shots physically fall in, not the partition they are stored in — so an exact polygon intersection silently misses boundary shots (observed in production on the EGI path: ~5% of one L3 partition's shots; see ``egi_h3_intersection``). ``expand_ring=1`` (default) closes this by adding the grid_disk ring-1 neighbors of every polygon-intersecting cell, restricted to the candidate set (``h3_ids`` when given). This is guaranteed sufficient: the overhang (~0.18 x edge) is far smaller than one cell width, so the true storage partition of any shot inside a polygon is always that polygon's cell or an immediate neighbor. Callers that need the exact polygon intersection can pass ``expand_ring=0``. Parameters ---------- spatial : str | list | GeoSeries | GeoDataFrame | shapely geometry ROI as a vector-file path / "W,S,E,N" string, [W,S,E,N] bbox list, geopandas object, or shapely geometry (EPSG:4326). res : int H3 resolution of the candidate cells (ignored when ``h3_ids`` given). h3_ids : list, optional Candidate cell set (e.g. a database's partition ids). When given, both the intersection and the ring expansion are restricted to it. expand_ring : int, default 1 ``grid_disk`` radius for the overhang-safety expansion; 0 disables. """ from shapely.geometry import box from shapely.geometry.base import BaseGeometry import geopandas as gpd if isinstance(spatial, str): # Mirror the CLI's parse_region semantics so the Python API can # accept "region.shp" / "region.gpkg" / "region.geojson" / "W,S,E,N" # the same way `gh3_extract -r` does. The gh3_load docstring example # advertises this. Without it, sindex.query() raises a misleading # "Array should be of object dtype" downstream. from .cliutils import parse_region spatial = parse_region(spatial) if isinstance(spatial, list): spatial = box(*spatial) elif isinstance(spatial, gpd.GeoSeries) or isinstance(spatial, gpd.GeoDataFrame): spatial = spatial.to_crs(4326).union_all() elif not isinstance(spatial, BaseGeometry): raise TypeError( f"Unsupported region type {type(spatial).__name__}; " "expected a path/bbox-string, a list [W,S,E,N], a GeoDataFrame/GeoSeries, or a shapely geometry." ) full_h3_list = h3_ids if h3_ids is None: full_h3_list = get_all_h3_hexagons(res) full_h3_geo = [fix_h3_geometry(i) for i in full_h3_list] h3_geo = gpd.GeoSeries(full_h3_geo, index=full_h3_list, crs=4326) h3_intersects = h3_geo.sindex.query(spatial, predicate='intersects') hits = h3_geo.index[h3_intersects].unique().tolist() if expand_ring and hits: # Restrict to the candidate set when given (only partitions that # exist); against the full grid every neighbor is valid by # construction, so skip building the 41k+-entry set. valid = set(h3_ids) if h3_ids is not None else None hits = h3_expand_ring(hits, ring=expand_ring, valid=valid) return hits
[docs] def h3_index_df(df, res=12, part=3, lat_col='lat_lowestmode', lon_col='lon_lowestmode'): import warnings import pandas as pd import h3pandas # h3pandas's geo_to_h3 / h3_to_parent call frame.assign() internally, # which on wide GEDI frames (~30+ columns) triggers pandas's fragmentation # heuristic and emits a PerformanceWarning on every per-task call. The # final df.copy() below defragments the result so the warning is purely # cosmetic; suppress it to keep worker logs clean. with warnings.catch_warnings(): warnings.filterwarnings( 'ignore', message='DataFrame is highly fragmented', category=pd.errors.PerformanceWarning, ) df = df.dropna(subset=[lat_col, lon_col]) df = df.reset_index() df = df.h3.geo_to_h3(res, lat_col=lat_col, lng_col=lon_col, set_index=True) df = df.h3.h3_to_parent(part) return df.copy()
[docs] def h3_parts_to_gdf(h3_ids, crs=4326): """ Convert H3 partition IDs to a GeoDataFrame with polygon geometries. Parameters ---------- h3_ids : list List of H3 cell IDs (strings) crs : int or str Output CRS (default: 4326) Returns ------- GeoDataFrame GeoDataFrame indexed by H3 ID with polygon geometries """ import geopandas as gpd geometries = [fix_h3_geometry(h) for h in h3_ids] gdf = gpd.GeoDataFrame( {'h3_id': h3_ids}, geometry=geometries, crs=4326 ).set_index('h3_id') if crs != 4326: gdf = gdf.to_crs(crs) return gdf