gedih3.gh3builder#
Attributes#
Functions#
|
Download GEDI HDF5 files in SOC (Science Operation Center) format. |
|
ETL-style S3 build: search for GEDI granules, then dispatch Dask workers |
|
Write a DataFrame to H3-partitioned parquet files. |
|
Write a sidecar metadata JSON file for an H3 partition parquet file. |
|
Read the sidecar metadata JSON for an H3 partition parquet file. |
|
Merge per-year metadata files into a single summary for an H3 partition. |
|
Check whether an H3 partition already contains data from a GEDI granule. |
|
Add a |
|
|
|
Merge multiple parquet files for an H3 partition into a single file. |
|
|
|
L1 resume pre-clean: act on every recorded |
Fold the merge-failed-granules sidecar into the build-log's granule |
|
|
The source release of granule products, read from the database's rows (#67). |
|
How to abandon an unfinished replacement, and what that leaves. |
|
Granules a replacement cannot tell old rows from new ones for (#68). |
|
Why the granules of |
|
Refuse to replace a granule whose changed rows cannot be told from its old ones. |
|
Path of the journal of a granule replacement under a build tmp dir. |
|
The target set of an unfinished replacement, or |
|
Build an H3-indexed GEDI database from local SOC files or S3 download. |
|
Merge two build log files from separate databases. |
Module Contents#
- gedih3.gh3builder.logger#
- gedih3.gh3builder.download_soc(product_vars: Dict, spatial=None, temporal=None, direct_access=False, update=False, version=None, odir=GH3_DEFAULT_SOC_DIR, n_jobs=5, on_granule_complete=None, ensure_l2a=True)[source]#
Download GEDI HDF5 files in SOC (Science Operation Center) format.
Expands variable specifications, ensures L2A essentials and shot_number are included, then delegates to
gedi_downloadfor retrieval.- Parameters:
- product_varsdict
Mapping of GEDI product codes (e.g., ‘L2A’, ‘L4A’) to variable lists. Accepts ‘default’, ‘minimal’, or explicit variable names.
- spatialGeoDataFrame, list, or str, optional
Spatial filter (vector file, bbox as [W,S,E,N], or ISO3 code).
- temporaltuple of str, optional
Temporal range as (start_date, end_date) in ‘YYYY-MM-DD’ format.
- direct_accessbool, default False
If True, use S3 streaming instead of downloading to disk.
- updatebool, default False
If True, resume a previous download (skip already-downloaded files).
- versionint or str, optional
GEDI data version (e.g., 3). If None, uses the package default (
GEDI_DEFAULT_VERSION).- odirstr
Output directory for downloaded HDF5 files.
- n_jobsint, default 5
Number of parallel download workers.
- on_granule_completecallable, optional
Callback
(granule_info_dict, status_str) -> Nonefor per-granule progress tracking. Passed through togedi_download(); see its docstring for thegranule_info_dictcontract (includes apathkey).- ensure_l2abool, default True
If True, automatically add L2A essentials when L2A is not in product_vars. Set to False for variable-only updates where L2A data already exists in the target database.
- Returns:
- list
List of downloaded SOC file paths or EarthAccessFile objects.
- gedih3.gh3builder.s3_etl_subset(product_vars, spatial=None, temporal=None, version=None, odir=None, ensure_l2a=True)[source]#
ETL-style S3 build: search for GEDI granules, then dispatch Dask workers to open remote HDF5 files, extract only selected variables via range requests, and write compact local HDF5 files.
This transfers and stores significantly less data than full downloads (10-50x smaller depending on variable selection). Workers run as true separate processes via Dask (GIL-free), matching the parallelism of download mode.
- Parameters:
- product_varsdict
Mapping of GEDI product codes (e.g., ‘L2A’, ‘L4A’) to variable lists. Accepts ‘default’, ‘minimal’, or explicit variable names.
- spatialGeoDataFrame, list, or str, optional
Spatial filter (vector file, bbox as [W,S,E,N], or ISO3 code).
- temporaltuple of str, optional
Temporal range as (start_date, end_date) in ‘YYYY-MM-DD’ format.
- versionint or str, optional
GEDI data version. If None, uses the package default (
GEDI_DEFAULT_VERSION).- odirstr
Output directory for compact local HDF5 files in SOC structure.
- ensure_l2abool, default True
If True, automatically add L2A essentials when L2A is not in product_vars. Set to False for variable-only updates where L2A data already exists in the target database.
- Returns:
- str
Path to the output directory containing compact HDF5 files.
- Raises:
- GediFileError
If no GEDI files are found on S3 for the given parameters.
- gedih3.gh3builder.h3_part_files(df, dir_path, res=12, part=3, lat_col='lat_lowestmode', lon_col='lon_lowestmode', roi_tiles=[])[source]#
Write a DataFrame to H3-partitioned parquet files.
Indexes the DataFrame by H3 cell, groups rows by partition cell, and writes each group as a separate parquet file. If a file already exists for a partition, new rows are appended.
- Parameters:
- dfpandas.DataFrame
Input DataFrame with coordinate columns and
root_beam,root_filemetadata columns.- dir_pathstr
Base directory for output partition subdirectories.
- resint, default 12
H3 resolution for shot-level indexing.
- partint, default 3
H3 resolution for file partitioning.
- lat_colstr, default ‘lat_lowestmode’
Column name for latitude values.
- lon_colstr, default ‘lon_lowestmode’
Column name for longitude values.
- roi_tileslist of str, optional
If non-empty, only write partitions whose H3 cell ID is in this list.
- Returns:
- list of str or None
List of written parquet file paths, or None if the input is empty.
- gedih3.gh3builder.h3_write_metadata(h3_file, stats=None)[source]#
Write a sidecar metadata JSON file for an H3 partition parquet file.
- Parameters:
- h3_filestr
Path to the H3 partition parquet file.
- statsdict, optional
Pre-computed stats from
parquet_merge_files’s streaming pass:{'shot_count', 'shot_min', 'shot_max', 'dt_min', 'dt_max', 'root_files'}. When provided, skips the ~1.5-2 GBpd.read_parquetre-read of the just-written merged file. WhenNoneor any field isNone, falls back to reading the columns from disk (slower, more memory).
- Returns:
- str
Path to the written metadata JSON file (
*PARTITION_META_FILENAME).
- gedih3.gh3builder.h3_read_metadata(h3_file)[source]#
Read the sidecar metadata JSON for an H3 partition parquet file.
- Parameters:
- h3_filestr
Path to the H3 partition parquet file. The metadata file is expected at the same path with a
PARTITION_META_FILENAMEextension.
- Returns:
- dict or None
Parsed metadata dictionary, or None if the metadata file does not exist.
- gedih3.gh3builder.h3_merge_metadata(h3_subdir)[source]#
Merge per-year metadata files into a single summary for an H3 partition.
Aggregates shot counts, expands shot and date ranges, and deduplicates granule identifiers across all year subdirectories within one H3 cell.
- Parameters:
- h3_subdirstr
Path to an H3 partition directory (e.g.,
h3_03=<cell_id>/) containing year subdirectories with parquet and metadata files.
- Returns:
- str or None
Path to the merged metadata JSON file, or None if no metadata files were found. With no year parquet left, a summary from an earlier merge is removed, and so is the partition directory when nothing else is left in it.
- gedih3.gh3builder.h3_skip_part(h3_dir, h3_part, gedi_file, cols=None)[source]#
Check whether an H3 partition already contains data from a GEDI granule.
Reads the partition’s merged metadata to determine if the granule has already been indexed and, optionally, if the requested columns are already present.
- Parameters:
- h3_dirstr
Root directory of the H3 database.
- h3_partstr
H3 cell ID of the partition to check.
- gedi_filestr
Path or filename of the GEDI HDF5 file to test against.
- colslist of str, optional
If provided, also verify that these columns already exist in the partition. The partition is only skipped when both the granule is present and all requested columns are available.
- Returns:
- bool
True if the partition should be skipped (granule already indexed and columns present), False otherwise.
- gedih3.gh3builder.h3_add_skip_column(df, h3_dir)[source]#
Add a
_skipboolean column indicating partitions to skip.Designed for use with
dask.dataframe.map_partitions. Checks each unique H3 partition cell individually, since a single beam can span multiple H3 cells. Delegates toh3_skip_partper cell.- Parameters:
- dfpandas.DataFrame
A single Dask partition containing H3 partition column(s) (
h3_XX) androot_file_l2a.- h3_dirstr
Root directory of the H3 database.
- Returns:
- pandas.DataFrame
Input DataFrame with an added
_skipcolumn (True if the cell’s granule data already exists in the database).
- gedih3.gh3builder.dh3_merge_metadata(h3_subdir)#
- gedih3.gh3builder.h3_merge_files(in_dir, out_dir, rm_src=True, replace=False, replace_granules=None)[source]#
Merge multiple parquet files for an H3 partition into a single file.
Reads all parquet files in
in_dir, merges them (deduplicating shots when appending to an existing file), writes the result toout_dir, and generates a sidecar metadata JSON file.- Parameters:
- in_dirstr
Input directory containing parquet fragment files for one H3 cell/year combination.
- out_dirstr
Output directory where the merged file will be written, preserving the
<h3_cell>/<year>/structure.- rm_srcbool, default True
If True, remove the source directory after a successful merge.
- replacebool, default False
If True, overwrite existing output files. If False, merge new data into any existing output file.
- replace_granulesdict, optional
{(orbit, granule, track): {product: basename}}: re-index these granules (#47). The existing file’s rows from them are dropped unless they already come from the given files, and the fragments’ rows take their place (parquet_merge_files). The existing file is rewritten even whenin_dirholds no fragment (the granules’ shots left this cell-year), the “already merged” skip never applies, and a file left with no row is removed with its metadata (and its year directory, when empty). Re-running gives the same result. An unreadable existing file is handled as on any merge: the fragments alone are written (without fragments, it is left as it is). Not withreplace=True(GediValidationError).
- Returns:
- str or None
Path to the merged parquet file, or None if
in_dircontained no parquet files and there was no orphaned merge result to promote (see_promote_orphan_merge_result), or the file was left empty byreplace_granulesand removed.
- gedih3.gh3builder.dh3_merge_files(in_dir, out_dir, rm_src=True, replace=False)#
- gedih3.gh3builder.preclean_merge_failures(tmp_dir: str) Dict[str, int][source]#
L1 resume pre-clean: act on every recorded
_merge_failuressentinel.For each partition the prior run’s merge marked failed:
Stat each
*.parquetin the partition — unlink if 0-byte (the canonical class B artifact). Truncated-but-nonzero parquets (class C) are unlinked only whenpq.ParquetFilecannot open them — checked cheaply by opening the footer once. The cost is bounded by the failure list, never the full healthy tree. Each unlinked fragment’s_complete/sentinel goes too, so the next Stage 1 re-runs that (granule × beam) instead of skipping it as done.Unlink any co-located
*.tmp/*.merge.tmpsiblings. These survive SIGKILL whenAtomicFileWriter.__exit__never runs and would otherwise pollute future merges.Delete the failure sentinel itself so re-running the pre-clean is idempotent (next run only re-acts on freshly-failed merges).
Step 1 probes each fragment in O(1) (
_parquet_tail_ok: the magic bytes at both ends and a plausible footer length), which catches 0-byte, truncated and never-finished files. The full footer parse is reserved for partitions whose recorded error is a footer-level artifact (thrift) or is missing: parsing a 1.4k-column footer costs seconds per fragment on GPFS, and a serial parse of every fragment behind a 14k-sentinel backlog (e.g. a batch of schema failures, whose fragments are healthy) was ~58 h on the driver. Partitions fan out over the registered dask Client.Returns
{'partitions_cleaned': N, 'parquets_removed': N, 'tmps_removed': N}. Companion toapply_merge_failures_to_logger— calling this without the granule flip-back would unlink fragments and leave their granules marked INDEXED, permanently dropping rows.build_h3dbruns it before Stage 1 (so the lost tasks are re-extracted in the same run) and_merge_and_finalizeagain at merge entry.
- gedih3.gh3builder.apply_merge_failures_to_logger(h3_logger, tmp_dir: str) int[source]#
Fold the merge-failed-granules sidecar into the build-log’s granule status: every recorded granule becomes
MERGE_FAILED. Returns the number of entries changed.Any prior status flips, not only
INDEXED: a granule extracted by the failing run is stillPENDINGhere, andset_post_build_infowould otherwise mark itINDEXEDfrom its rows in other partitions. A recorded granule the log does not track yet (S3 mode registers none) is added.set_post_build_info, the reconcile andlog_stateall keepMERGE_FAILED; only_release_merge_failed(), after a Stage 1 run that re-extracted the granule, clears it.Idempotent. Removes the sidecar after the fold. The CLI calls it at startup (a run killed before its fold leaves the sidecar behind) and right after every merge. Does NOT call
h3_logger.save_log— the caller controls save cadence.
- gedih3.gh3builder.releases_from_rows(h3_dir: str, products, keys, version: int | None = None)[source]#
The source release of granule products, read from the database’s rows (#67).
A database built before gedih3 0.20.10 records no release per granule, so re-released granules cannot be found from the build log. Every row names its source file per product (
root_file_<prod>), and the file name holds the granule key and the release (PPDS, PGE, generation, suffix): one worker task per (cell, year) file reads its metadata and, if it lists one ofkeys, those columns only (_year_file_source_releases()). The files come from the database’s_manifest.txt(smart_glob()).- Parameters:
- h3_dirstr
Database root.
- productsiterable of str
Products to read, e.g.
['L2A', 'L4A']; one without a source column is skipped.- keysiterable of tuple
(orbit, granule, track)of the granules to read.- versionint, optional
The database’s GEDI version: names of another one are ignored.
- Returns:
- tuple
(releases, mixed, unread, failed):{key: {product: tag}}for products whose rows name one release;{key: {product: [tags]}}for those naming several (a replacement left half done: never guessed); the keys a file that could not be read may hold, left out of both (their rows were not all read: all ofkeyswhen that cannot be told from the file’s metadata); and the files that could not be read (skipped and logged;Nonefor a batch of them). Requires a registered dask Client.
- gedih3.gh3builder.replacement_abandon_hint(tmp_dir: str) str[source]#
How to abandon an unfinished replacement, and what that leaves.
- gedih3.gh3builder.blind_replacement_targets(h3_dir: str, targets, unsourced_by_key: Dict[Tuple[int, int, int], set] | None = None) Dict[Tuple[int, int, int], List[str]][source]#
Granules a replacement cannot tell old rows from new ones for (#68).
A replacement merge keeps a row when every
root_file_<prod>column names the given file. A product added by a variable update of an older gedih3 has no such column, so when it is the only product of a granule with a new release, the merge would keep the old rows while the build log records the new release. A granule with another product re-released is fine: its rows are replaced whole. One with no recorded release (indexed before gedih3 recorded them) cannot be shown to be fine, so it counts. Answered from the build log (h3_columns, the union of every file’s columns, and the recorded releases), and fromunsourced_by_keyfor files that differ.- Parameters:
- h3_dirstr
Database root.
- targetsdict
{(orbit, granule, track): {product: file}}, as passed to_replace_granules_build().- unsourced_by_keydict, optional
{(orbit, granule, track): products}lacking a source column in a file holding that granule (a database expanded after a product was added without one has it in some files only), from the files’ footers (_year_file_replace_targets()).
- Returns:
- dict
{(orbit, granule, track): [product, ...]}, the unsourced products of each such granule.
- gedih3.gh3builder.blind_replacement_message(blind, tmp_dir: str | None = None) str[source]#
Why the granules of
blind_replacement_targets()are not replaced.
- gedih3.gh3builder.check_replacement_sources(h3_dir: str, targets, tmp_dir: str | None = None, unsourced_by_key: Dict[Tuple[int, int, int], set] | None = None) None[source]#
Refuse to replace a granule whose changed rows cannot be told from its old ones.
See
blind_replacement_targets(); the parameters are the same, andtmp_dir(the build tmp dir) points the message at an unfinished replacement’s journal.- Raises:
- GediValidationError
If such a granule is among
targets. Nothing was changed.
- gedih3.gh3builder.replacement_journal(tmp_dir: str) str[source]#
Path of the journal of a granule replacement under a build tmp dir.
Its presence means a replacement started there and has not finished:
gh3_buildresumes it before anything else.
- gedih3.gh3builder.read_replacement_journal(tmp_dir: str) Dict[Tuple[int, int, int], Dict[str, str]] | None[source]#
The target set of an unfinished replacement, or
Nonewhen there is none.{(orbit, granule, track): {product: basename}}, as passed to_replace_granules_build().
- gedih3.gh3builder.build_h3db(product_vars: Dict[str, List[str]], res: int = 12, part: int = 3, spatial=None, temporal=None, soc_source: str | List | None = None, version: int | None = None, tmp_dir: str = os.path.join(GH3_DEFAULT_TMP_DIR, 'gh3_build'), h3_dir: str = GH3_DEFAULT_H3_DIR, skip_granules: List[Dict] | None = None, status_callback: Callable[[str], None] | None = None, variable_only_update: bool = False, exclude: List[str] | None = None, allow_missing_products: bool = False, granule_products_callback: Callable[[Dict[Tuple[int, int, int], List[str]]], None] | None = None, stage1_outcome_callback: Callable[[set, set], None] | None = None, only_granules: List[Dict] | None = None, releases_callback: Callable[[Dict[Tuple[int, int, int], Dict[str, str]]], None] | None = None) List[str] | None[source]#
Build an H3-indexed GEDI database from local SOC files or S3 download.
- Parameters:
- product_varsdict
Dictionary mapping GEDI product codes (e.g., ‘L2A’, ‘L4A’) to lists of variable names to extract. Use ‘default’, ‘minimal’, or explicit lists.
- resint, default 12
H3 resolution level for indexing individual shots (0-15).
- partint, default 3
H3 resolution level for file partitioning (0-15, must be <= res).
- spatialGeoDataFrame, list, or str, optional
Spatial filter for the region of interest.
- temporaltuple, optional
Temporal filter as (start_date, end_date) in ‘YYYY-MM-DD’ format.
- soc_sourcestr, list, or None
None: download from NASA S3 to a temp directory (cleaned up after build)str: path to local directory containing GEDI SOC HDF5 fileslist: pre-acquired list of file paths or EarthAccessFile objects
- versionint or None
GEDI data release for every product.
Noneresolves via_resolve_build_version()(build log > SOC tree > package default). Every SOC listing is filtered to this release, so the Stage 1 schema is derived from files of exactly that release.- tmp_dirstr
Path to temporary directory for intermediate files.
- h3_dirstr
Output path for the H3-indexed parquet database.
- skip_granuleslist of dict, optional
List of granule identifiers to skip (from previous builds).
- status_callbackcallable, optional
Called with status string at pipeline stage transitions.
- variable_only_updatebool, default False
If True, only add new variable columns to existing partition files via shot_number join (
_build_add_variables). Skips full pipeline. The caller (CLI) determines this from the build logger state.- excludelist of str, optional
fnmatch patterns of source file basenames to ignore (e.g.
['*_SGS.h5']), in every listing. The patterns kept in an existing database’s build log (gh3_build --exclude) are always added.- allow_missing_productsbool, default False
Admit granules that have L2A but lack later products (L2B, L4A… published after L2A). Their rows store those products’ columns as null, in the database’s types, until
_build_fill_products()backfills them. Requires the streaming writer. Default: a granule must have every requested product.- granule_products_callbackcallable, optional
Called once, before any fragment is written, with
{(orbit, granule, track): [missing products]}for every granule this Stage 1 processes (an empty list when it has every product). The CLI recordsMISSING_SOURCEfrom it, so the backfill knows its targets without scanning the database, even after a crash mid-write.- stage1_outcome_callbackcallable, optional
Called as
(incomplete_keys, empty_keys)once Stage 1 drained every task (see_write_partitioned_streaming()), before the merge. Never called by the legacy writer (GH3_WRITE_STREAMINGoff, deprecated), by an interrupted drain, or when no granule is left to process.- only_granuleslist of dict, optional
If given, Stage 1 reads only these granules and leaves every other one for a later build (
H3BuildLogger.stage1_only_granules: a spatial-only expansion reads only the added region).- releases_callbackcallable, optional
variable_only_updateonly: called before the first write with the source releases ({(orbit, granule, track): {product: tag}}) read for each product new to the database, which gets source columns too (see_build_add_variables()), so the caller can record them (H3BuildLogger.record_releases()).
- Returns:
- list of str or None
List of output parquet file paths, or None if no new data processed.
- Raises:
- H3ValidationError
If H3 resolution or partition parameters are invalid
- GediFileError
If local source directory doesn’t exist or S3 returns no files
- gedih3.gh3builder.merge_build_logs(log_file_1: str, log_file_2: str, output_log_file: str) dict[source]#
Merge two build log files from separate databases.
Combines granules, columns, partition IDs, and date ranges while validating configuration consistency (gedi_version, h3_resolution_level, h3_partition_level).
- Parameters:
- log_file_1str
Path to first build log (base log)
- log_file_2str
Path to second build log (log to merge in)
- output_log_filestr
Path to write merged build log
- Returns:
- dict
Merged log dictionary
- Raises:
- FileNotFoundError
If either log file does not exist
- ValueError
If log configurations are incompatible (different gedi_version, h3_resolution_level, or h3_partition_level)
Examples
>>> merged_log = merge_build_logs( ... f'/path/to/database_world/{BUILD_LOG_FILENAME}', ... f'/path/to/database_world_a10/{BUILD_LOG_FILENAME}', ... f'/path/to/database_world_merged/{BUILD_LOG_FILENAME}' ... )