#! python
# 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
"""
GEDI H3/EGI Data Extraction Tool
Extract and filter GEDI shots from H3-indexed parquet database with spatial,
temporal, and quality filters. Supports H3 or EGI output indexing/partitioning.
"""
import os
import sys
import argparse
[docs]
def get_cmd_args():
"""Parse command line arguments for GEDI data extraction"""
from gedih3.cliutils import add_dask_args, add_verbosity_args, add_product_args, add_storage_args, parse_egi_levels
p = argparse.ArgumentParser(
description="Extract and filter GEDI shots with H3 or EGI spatial indexing"
)
# Database/output configuration
p.add_argument("-d", "--database", dest="database", type=str, default=None,
help="path to H3 database directory")
p.add_argument("-o", "--output", dest="output", required=True, type=str,
help="output directory or file path")
p.add_argument("-f", "--format", dest="format", type=str, default='parquet',
help="output format [default=parquet]")
p.add_argument("-m", "--merge", dest="merge", action='store_true',
help="merge all partitions into single file")
# Indexing options
p.add_argument("-egi", "--egi", dest="egi", type=parse_egi_levels, default=None,
nargs='?', const=(1, 12),
help="EGI indexing: bare flag defaults to 1:12, or 'index[:partition]' e.g., '1' or '6:12'")
p.add_argument("--egi-shuffle", dest="egi_shuffle", action='store_true',
help="Use shuffle-based EGI extraction (gh3_load + egi_extract) instead of direct loading")
# Spatial/temporal filtering
p.add_argument("-r", "--region", dest="region", type=str, default=None,
help="vector file, bbox 'W,S,E,N', or ISO3 code")
p.add_argument("-t0", "--time-start", dest="time_start", type=str, default=None,
help="start date [YYYY-MM-DD]")
p.add_argument("-t1", "--time-end", dest="time_end", type=str, default=None,
help="end date [YYYY-MM-DD]")
# Variable selection
p.add_argument("-l", "--list", dest="list", nargs='+', type=str, default=None,
help="variables to export (space-separated, file path, or wildcards like 'rh_*_l2a')")
add_product_args(p, include_detail_level=False)
# Options
p.add_argument("-g", "--geo", dest="geo", action='store_true',
help="export as georeferenced points")
p.add_argument("-t", "--time", dest="add_datetime", action='store_true',
help="add datetime column to output")
p.add_argument("-q", "--query", dest="query", type=str, default=None,
help="pandas query string for filtering")
p.add_argument("-y", "--quality", dest="quality", action='store_true',
help="apply quality filtering")
p.add_argument("-b", "--beam-type", dest="beam_type", type=str, default=None,
choices=["power", "coverage"],
help="filter by beam type: 'power' (full-power beams) or 'coverage' (coverage beams)")
# Dask, storage, and verbosity
add_dask_args(p)
add_storage_args(p)
add_verbosity_args(p)
return p.parse_args()
[docs]
def main():
args = get_cmd_args()
# Import cli_exception_handler early for wrapping the main logic
from gedih3.cliutils import cli_exception_handler
with cli_exception_handler(args):
from dask.distributed import Client
import gedih3.gh3driver as gh3
from gedih3.cliutils import (collect_columns, build_query_string, parse_region,
parse_dask_args, setup_logging, print_banner,
print_success, configure_database_path, h3_col_name,
resolve_path_args, setup_storage)
# Parse EGI levels if specified
use_egi = args.egi is not None
if use_egi:
egi_index_level, egi_partition_level = args.egi
else:
egi_index_level, egi_partition_level = None, None
# Setup logging and print banner
logger = setup_logging(args, __name__)
setup_storage(args, logger=logger)
title = "GEDI EGI Data Extraction Tool" if use_egi else "GEDI H3 Data Extraction Tool"
print_banner(title, logger=logger)
resolve_path_args(args, ['database', 'output'], logger=logger)
# Configure database path
configure_database_path(args, logger=logger)
# Verify database exists
from gedih3.utils import smart_exists
if not smart_exists(args.database):
logger.error(f"Database directory not found: {args.database}")
logger.error("Please specify a valid database path with -d/--database")
sys.exit(1)
# Read metadata
from gedih3.config import BUILD_LOG_FILENAME
from gedih3.utils import smart_join
if not smart_exists(smart_join(args.database, BUILD_LOG_FILENAME)):
raise FileNotFoundError("Could not read database metadata. Invalid database?")
# Parse region
region = None
if args.region:
logger.info(f"Parsing region: {args.region}")
region = parse_region(args.region)
# Collect columns
logger.info("Collecting variables...")
columns = collect_columns(args)
# EGI indexing works best with Point geometry from GeoDataFrame
# Ensure geometry column is loaded so we have coordinate information
if use_egi and 'geometry' not in columns:
columns.append('geometry')
if len(columns) > 0:
logger.info(f" Total variables: {len(columns)}")
else:
raise ValueError("No variables selected for extraction. Please specify variables with -l/--list or product-specific options.")
# Build query
query_str = build_query_string(args)
if query_str:
logger.info(f"Query filter: {query_str}")
dask_kwargs = parse_dask_args(args)
with Client(**dask_kwargs) as client:
logger.info(f"Dask dashboard available at: {client.dashboard_link}")
# Determine partition column and process data
if use_egi:
from gedih3.egi.config import egi_col_name, get_resolution
index_res = get_resolution(egi_index_level)
partition_res = get_resolution(egi_partition_level)
logger.info(f" Index level: {egi_index_level} (~{index_res:.0f}m)")
logger.info(f" Partition level: {egi_partition_level} (~{partition_res:.0f}m)")
egi_part_col = egi_col_name(egi_partition_level)
if args.egi_shuffle:
# Shuffle-based approach: gh3_load + egi_extract
# More reliable but slower for large datasets
logger.info("Loading H3 data then converting to EGI (shuffle-based)...")
ddf_h3 = gh3.gh3_load(
columns=columns,
region=region,
query=query_str,
source=args.database
)
logger.info(f" Loaded {ddf_h3.npartitions} H3 partitions")
logger.info(" Converting to EGI (shuffling data)...")
ddf = gh3.egi_extract(
ddf_h3,
index_level=egi_index_level,
partition_level=egi_partition_level,
add_geometry=True
)
else:
# Direct loading approach: egi_load
# Faster but may have issues with complex geometries
logger.info("Loading data directly into EGI partitions (no shuffle)...")
ddf = gh3.egi_load(
columns=columns,
region=region,
query=query_str,
source=args.database,
index_level=egi_index_level,
partition_level=egi_partition_level
)
logger.info(f" Loaded {ddf.npartitions} EGI partitions")
part_col = egi_part_col
else:
# H3 mode - load normally
logger.info("Loading data from H3 database...")
ddf = gh3.gh3_load(
columns=columns,
region=region,
query=query_str,
source=args.database
)
logger.info(f" Loaded {ddf.npartitions} partitions")
part = gh3.gh3_read_meta('h3_partition_level', gh3_root_dir=args.database)
part_col = h3_col_name(part)
# Export - use simplified flat file structure (not hive-partitioned)
logger.info("Exporting data...")
logger.info(f" Output format: simplified flat files by {part_col}")
meta_kwargs = {'query_filter': query_str}
if use_egi:
meta_kwargs['egi_index_level'] = egi_index_level
meta_kwargs['egi_partition_level'] = egi_partition_level
else:
meta_kwargs['h3_partition_level'] = gh3.gh3_read_meta('h3_partition_level', gh3_root_dir=args.database)
gh3.gh3_export(
ddf, output=args.output, fmt=args.format, merge=args.merge,
show_progress=not getattr(args, 'quiet', False),
drop_internal=False,
source_database=args.database, tool='gh3_extract',
**meta_kwargs
)
print_success(f"Data exported to {args.output}", logger=logger)
if __name__ == '__main__':
main()