Source code for gedih3.egi.core

# 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

"""
EGI (EASE Grid Index) Core Module

This module provides the fundamental hash encoding and decoding functions for
the EGI spatial indexing system. The hash encodes spatial location and resolution
level into a single uint64 value.

Hash Structure (uint64):
    level * 1e18 + px_outer * 1e15 + py_outer * 1e12 + px_inner * 1e6 + py_inner

    - level (1-12): Resolution level encoded in digits 19-20
    - px_outer: Outer tile X index (0-215) encoded in digits 16-18
    - py_outer: Outer tile Y index (0-90) encoded in digits 13-15
    - px_inner: Inner pixel X index within tile encoded in digits 7-12
    - py_inner: Inner pixel Y index within tile encoded in digits 1-6

Data Type Considerations:
    - Hash values MUST use np.uint64 for 64-bit precision
    - Intermediate calculations use appropriate uint16/uint32 to prevent overflow
    - Coordinate divisions must use exact type casting to preserve precision
"""
from typing import List, Tuple, Union
import numpy as np
from numpy.typing import NDArray

from .config import LIMITS, RESOLUTIONS, OUTER_RES, validate_level


[docs] def hasher( level: Union[int, NDArray[np.integer]], px_outer: Union[int, NDArray[np.uint16]], py_outer: Union[int, NDArray[np.uint16]], px_inner: Union[int, NDArray[np.uint32]], py_inner: Union[int, NDArray[np.uint32]] ) -> Union[np.uint64, NDArray[np.uint64]]: """ Construct EGI hash from component parts. This is the core hash construction function. It combines level, outer tile coordinates, and inner pixel coordinates into a single uint64 hash. Parameters ---------- level : int or array Resolution level (1-12) px_outer : int or array Outer tile X index (0-215) py_outer : int or array Outer tile Y index (0-90) px_inner : int or array Inner pixel X index within tile py_inner : int or array Inner pixel Y index within tile Returns ------- np.uint64 or ndarray of uint64 EGI hash value(s) Notes ----- All inputs must be the same shape or broadcastable. The function preserves exact integer arithmetic using uint64 throughout. """ # Convert to uint64 for multiplication to prevent overflow uint_hash = ( np.uint64(level) * np.uint64(1e18) + np.uint64(px_outer) * np.uint64(1e15) + np.uint64(py_outer) * np.uint64(1e12) + np.uint64(px_inner) * np.uint64(1e6) + np.uint64(py_inner) ) return uint_hash
[docs] def to_hash( x: Union[float, NDArray[np.floating]], y: Union[float, NDArray[np.floating]], level: int = 1 ) -> Union[np.uint64, NDArray[np.uint64]]: """ Convert EPSG:6933 coordinates to EGI hash. Parameters ---------- x : float or array X coordinate(s) in EPSG:6933 (meters from origin) y : float or array Y coordinate(s) in EPSG:6933 (meters from origin) level : int Target EGI resolution level (1-12), default=1 Returns ------- np.uint64 or ndarray of uint64 EGI hash value(s) Examples -------- >>> # Single coordinate >>> hash_val = to_hash(-8000000.0, 4000000.0, level=6) >>> >>> # Array of coordinates >>> x = np.array([-8000000.0, -7000000.0]) >>> y = np.array([4000000.0, 3500000.0]) >>> hashes = to_hash(x, y, level=6) Notes ----- Outer-tile and inner-pixel indices are derived independently in the float domain — matching the reference easegridindex grid alignment so existing on-disk data stays interpretable — but we then carry any boundary overflow from ``px_inner`` into ``px_outer``. ``OUTER_RES`` (~160143.2 m) is not exactly representable in float64, so for coordinates at or just below a tile boundary, ``x_offset // OUTER_RES`` rounds down to tile N-1 while ``x_offset % OUTER_RES // scale`` returns ``SCALE_FACTOR`` (one past the valid inner range). Left uncorrected, those out-of-range inner indices propagate through ``to_parent`` as spurious non-zero inner indices at coarser levels, producing boundary-shadow cells at every level. The carry folds the overflow back into the proper outer tile so ``px_inner`` is always in ``[0, SCALE_FACTOR)``. Non-boundary inputs are unaffected. Coordinates must be in EPSG:6933 projection. Do NOT use this function with WGS84 coordinates - reproject first. """ validate_level(level) scale = RESOLUTIONS[level] scale_factor = round(OUTER_RES / scale) # integer pixels per outer tile at this level # Calculate coordinate offsets from grid origin x_offset = x - LIMITS['lon_w'] y_offset = y - LIMITS['lat_s'] # Outer-tile and inner-pixel indices, both via the same OUTER_RES grid the # reference easegridindex uses. This preserves backward-compatible cell # alignment for existing data — we only diverge for boundary overflow below. px_outer = (x_offset // OUTER_RES).astype(np.int64) if hasattr(x_offset, 'astype') else np.int64(x_offset // OUTER_RES) py_outer = (y_offset // OUTER_RES).astype(np.int64) if hasattr(y_offset, 'astype') else np.int64(y_offset // OUTER_RES) px_inner = (x_offset % OUTER_RES // scale).astype(np.int64) if hasattr(x_offset, 'astype') else np.int64(x_offset % OUTER_RES // scale) py_inner = (y_offset % OUTER_RES // scale).astype(np.int64) if hasattr(y_offset, 'astype') else np.int64(y_offset % OUTER_RES // scale) # Boundary overflow carry: at tile boundaries, the independent float ops # leave px_inner == scale_factor (one past valid). Fold that overflow into # the next outer tile so the hash always satisfies 0 <= inner < scale_factor. px_carry = px_inner >= scale_factor py_carry = py_inner >= scale_factor px_outer = np.where(px_carry, px_outer + 1, px_outer) py_outer = np.where(py_carry, py_outer + 1, py_outer) px_inner = np.where(px_carry, 0, px_inner) py_inner = np.where(py_carry, 0, py_inner) return hasher( level, np.uint16(px_outer), np.uint16(py_outer), np.uint32(px_inner), np.uint32(py_inner), )
[docs] def from_hash( uint_hash: Union[np.uint64, NDArray[np.uint64]] ) -> Tuple[ Union[int, NDArray[np.integer]], Union[float, NDArray[np.floating]], Union[np.uint16, NDArray[np.uint16]], Union[np.uint16, NDArray[np.uint16]], Union[np.uint32, NDArray[np.uint32]], Union[np.uint32, NDArray[np.uint32]] ]: """ Decode EGI hash into its component parts. Parameters ---------- uint_hash : uint64 or array of uint64 EGI hash value(s) to decode Returns ------- tuple (level, scale, px_outer, py_outer, px_inner, py_inner) - level: Resolution level (1-12) - scale: Pixel size in meters - px_outer: Outer tile X index - py_outer: Outer tile Y index - px_inner: Inner pixel X index - py_inner: Inner pixel Y index Examples -------- >>> level, scale, px_o, py_o, px_i, py_i = from_hash(hash_val) """ uint_hash = np.uint64(uint_hash) # Extract level from highest digits level = uint_hash // np.uint64(1e18) # Handle scalar vs array for resolution lookup if np.ndim(level) == 0: scale = RESOLUTIONS.get(int(level)) else: scale = np.array([RESOLUTIONS.get(int(lv)) for lv in level]) # Extract inner pixel coordinates (lower digits) py_inner = np.uint32(uint_hash % np.uint32(1e6)) px_inner = np.uint32(uint_hash % np.uint64(1e12) // np.uint64(1e6)) # Extract outer tile coordinates (middle digits) py_outer = np.uint16(uint_hash % np.uint64(1e15) // np.uint64(1e12)) px_outer = np.uint16(uint_hash % np.uint64(1e18) // np.uint64(1e15)) return level, scale, px_outer, py_outer, px_inner, py_inner
[docs] def get_level(uint_hash: Union[np.uint64, NDArray[np.uint64]]) -> Union[int, NDArray[np.integer]]: """ Extract the resolution level from an EGI hash. Parameters ---------- uint_hash : uint64 or array of uint64 EGI hash value(s) Returns ------- int or array Resolution level(s) """ return np.uint64(uint_hash) // np.uint64(1e18)
[docs] def get_scale(uint_hash: Union[np.uint64, NDArray[np.uint64]]) -> Union[float, NDArray[np.floating]]: """ Get the pixel size in meters for an EGI hash. Parameters ---------- uint_hash : uint64 or array of uint64 EGI hash value(s) Returns ------- float or array Pixel size(s) in meters """ level = get_level(uint_hash) if np.ndim(level) == 0: return RESOLUTIONS.get(int(level)) return np.array([RESOLUTIONS.get(int(lv)) for lv in level])
[docs] def to_parent( uint_hash: Union[np.uint64, NDArray[np.uint64]], parent_level: int ) -> Union[np.uint64, NDArray[np.uint64]]: """ Convert EGI hash to a coarser (parent) resolution level. This function rescales the inner pixel coordinates to the parent resolution while preserving the outer tile coordinates. Parameters ---------- uint_hash : uint64 or array of uint64 EGI hash value(s) to convert parent_level : int Target parent resolution level (must be >= current level) Returns ------- uint64 or array of uint64 EGI hash(es) at parent resolution Raises ------ ValueError If parent_level is finer than current level Examples -------- >>> # Convert from level 1 to level 6 >>> parent_hash = to_parent(fine_hash, parent_level=6) """ validate_level(parent_level) uint_hash = np.uint64(uint_hash) level = get_level(uint_hash) # Validate that we're going to coarser resolution if np.ndim(level) == 0: if int(level) > parent_level: raise ValueError( f"Cannot convert to finer resolution. Current level: {level}, " f"requested parent level: {parent_level}" ) current_scale = RESOLUTIONS[int(level)] else: if np.any(level > parent_level): raise ValueError( f"Cannot convert to finer resolution. Some hashes have level > {parent_level}" ) current_scale = np.array([RESOLUTIONS[int(lv)] for lv in level]) parent_scale = RESOLUTIONS[parent_level] scale_factor = round(parent_scale / current_scale) if np.ndim(current_scale) == 0 else np.round(parent_scale / current_scale) # Rescale inner pixel coordinates py_inner = np.uint32(uint_hash % np.uint32(1e6) // scale_factor) px_inner = np.uint64(uint_hash % np.uint64(1e12) // np.uint32(1e6) // scale_factor) # Preserve outer tile coordinates p_outer = uint_hash % np.uint64(1e18) // np.uint64(1e12) # Reconstruct hash at parent level re_uint_hash = ( np.uint64(parent_level) * np.uint64(1e18) + np.uint64(p_outer) * np.uint64(1e12) + np.uint64(px_inner) * np.uint64(1e6) + py_inner ) return re_uint_hash
[docs] def get_children( uint_hash: Union[np.uint64, NDArray[np.uint64]], children_level: int = -1 ) -> List[np.uint64]: """ Get all child pixels at a finer resolution. Parameters ---------- uint_hash : uint64 EGI hash value (parent pixel) children_level : int Target resolution level for children. Negative values are relative to parent level (e.g., -1 = one level finer) Returns ------- list of uint64 EGI hashes of all child pixels Raises ------ ValueError If children_level is not finer than parent level Examples -------- >>> children = get_children(parent_hash, children_level=-1) >>> fine_children = get_children(level6_hash, children_level=1) """ uint_hash = np.uint64(uint_hash) level, scale, px_outer, py_outer, px_inner, py_inner = from_hash(uint_hash) level = int(level) if children_level < 0: children_level = level + children_level validate_level(children_level) if children_level >= level: raise ValueError( f"Children level ({children_level}) must be finer than parent level ({level})" ) # Compute pixel bounds directly (no Shapely dependency) minx = float(scale) * float(px_inner) + OUTER_RES * float(px_outer) + LIMITS['lon_w'] miny = float(scale) * float(py_inner) + OUTER_RES * float(py_outer) + LIMITS['lat_s'] maxx = minx + float(scale) maxy = miny + float(scale) children_scale = RESOLUTIONS[children_level] offset = children_scale / 2 # Generate grid of center points within parent pixel px = np.arange(minx + offset, maxx, children_scale) py = np.arange(miny + offset, maxy, children_scale) center_points = np.stack(np.meshgrid(px, py)).reshape(2, -1).T return [to_hash(x, y, children_level) for x, y in center_points]
[docs] def pixels_per_tile(uint_hash_or_level: Union[np.uint64, int]) -> Union[int, float]: """ Calculate the number of pixels per outer tile at a given level. Parameters ---------- uint_hash_or_level : uint64 or int Either an EGI hash or a resolution level (1-12) Returns ------- int or float Number of pixels along one edge of an outer tile """ if uint_hash_or_level <= 12: # Input is a level level = int(uint_hash_or_level) else: # Input is a hash level = int(np.uint64(uint_hash_or_level) // np.uint64(1e18)) scale = RESOLUTIONS[level] return OUTER_RES / scale
[docs] def validate_hash(uint_hash: Union[np.uint64, NDArray[np.uint64]]) -> bool: """ Validate that an EGI hash is well-formed. Parameters ---------- uint_hash : uint64 or array of uint64 EGI hash value(s) to validate Returns ------- bool True if valid, False otherwise """ uint_hash = np.uint64(uint_hash) level = get_level(uint_hash) if np.ndim(level) == 0: return 1 <= int(level) <= 12 return np.all((level >= 1) & (level <= 12))