Skip to content

Gridded ABM Hyetograph Generation

This notebook demonstrates how to generate spatially distributed precipitation hyetographs using the Alternating Block Method (ABM) applied pixel-by-pixel across a NOAA Atlas 14 grid. The output is a NetCDF file with dimensions (time, lat, lon) ready for direct import into HEC-RAS as a rain-on-grid 2D precipitation boundary condition.

Key Features: - Per-pixel temporal distribution using the ABM (Chow, Maidment, Mays 1988) - Spatial depth data from NOAA Atlas 14 CONUS NetCDF via HTTP byte-range requests - Sub-hourly intervals via centroid DDF ratios (no separate download required) - Fully vectorized across all grid pixels — large domains run in seconds - CF-1.8 compliant NetCDF output with precip_incremental and precip_cumulative variables

Example 1 — Direct Bounds: Generate a 100-year 24-hour ABM grid for a user-specified bounding box (Houston, TX area)

Example 2 — HEC-RAS Project Bounds: Derive the bounding box automatically from a HEC-RAS geometry HDF file

Alternative Workflow: Use generate_from_asc_files() with pre-downloaded NOAA Atlas 14 .asc grids

Python
# =============================================================================
# DEVELOPMENT MODE TOGGLE
# =============================================================================
USE_LOCAL_SOURCE = False  # <-- TOGGLE THIS

if USE_LOCAL_SOURCE:
    import sys
    from pathlib import Path
    local_path = str(Path.cwd().parent)
    if local_path not in sys.path:
        sys.path.insert(0, local_path)
    print(f"LOCAL SOURCE MODE: Loading from {local_path}/ras_commander")
else:
    print("PIP PACKAGE MODE: Loading installed ras-commander")

import ras_commander
print(f"Loaded: {ras_commander.__file__}")
Text Only
PIP PACKAGE MODE: Loading installed ras-commander


Loaded: G:\GH\ras-commander-pr224-update\ras_commander\__init__.py

Example 1 — Direct Bounds (Houston, TX)

The simplest workflow: provide a bounding box in decimal degrees and let AbmHyetographGrid.generate() handle everything — Atlas 14 download, sub-hourly gap filling, ABM temporal distribution, and NetCDF export.

Configuration: - Location: Houston, TX area - Return period: 100-year (ARI = 100) - Storm duration: 24 hours - Timestep: 15 minutes (96 intervals) - Peak position: 50% (centered)

Python
from pathlib import Path
from ras_commander.precip import AbmHyetographGrid

# --- Configuration ---
BOUNDS = (-95.5, 29.5, -95.0, 30.0)   # (west, south, east, north) decimal degrees WGS84
ARI_YEARS = 100                         # 100-year return period
STORM_DURATION_HR = 24.0               # 24-hour storm
TIMESTEP_MIN = 15                       # 15-minute intervals
PEAK_POSITION_PCT = 50.0               # peak at midpoint of storm
OUTPUT_NC = Path("abm_houston_100yr_24hr.nc")

# --- Generate ---
nc_path = AbmHyetographGrid.generate(
    bounds=BOUNDS,
    ari_years=ARI_YEARS,
    storm_duration_hours=STORM_DURATION_HR,
    timestep_minutes=TIMESTEP_MIN,
    peak_position_percent=PEAK_POSITION_PCT,
    output_netcdf=OUTPUT_NC,
)

print(f"Output: {nc_path}")
print(f"File size: {nc_path.stat().st_size / 1024:.1f} KB")
Text Only
2026-07-07 17:37:57 - ras_commander.precip.AbmHyetographGrid - INFO - Generating ABM grid: ARI=100yr, duration=24.0hr, dt=15min, [email protected]%, bounds=(-95.5, 29.5, -95.0, 30.0)


2026-07-07 17:37:57 - ras_commander.precip.Atlas14Grid - INFO - Loading Atlas 14 coordinate arrays from remote file...


2026-07-07 17:38:15 - ras_commander.precip.Atlas14Grid - INFO - Loaded coordinates: lat=3121, lon=7081, ari=9


2026-07-07 17:38:15 - ras_commander.precip.Atlas14Grid - INFO - Extracting Atlas 14 data for bounds (-95.5250, 29.4750, -94.9750, 30.0250): lat[658:724], lon[3538:3604]


2026-07-07 17:38:33 - ras_commander.precip.Atlas14Grid - INFO - Data transfer: 536.0 KB (vs 2655.6 MB full grid, 100.0% reduction)


2026-07-07 17:38:33 - ras_commander.precip.StormGenerator - INFO - Downloading Atlas 14 data for (29.75, -95.25)...


2026-07-07 17:38:35 - ras_commander.precip.StormGenerator - INFO - Downloaded Atlas 14 data for region: Texas


2026-07-07 17:38:35 - ras_commander.precip.AbmHyetographGrid - INFO - ABM grid complete → G:\GH\ras-commander-pr224-update\examples\abm_houston_100yr_24hr.nc


Output: G:\GH\ras-commander-pr224-update\examples\abm_houston_100yr_24hr.nc
File size: 1567.2 KB

1. Inspect the NetCDF Output

Open the NetCDF with xarray to examine dimensions, coordinates, and variables.

Python
import xarray as xr
import numpy as np

ds = xr.open_dataset(nc_path, decode_timedelta=False)
print(ds)
print()
print("Attributes:")
for k, v in ds.attrs.items():
    print(f"  {k}: {v}")
Text Only
<xarray.Dataset> Size: 3MB
Dimensions:             (time: 96, lat: 66, lon: 66)
Coordinates:
  * time                (time) float64 768B 0.0 0.25 0.5 ... 23.25 23.5 23.75
  * lat                 (lat) float64 528B 29.48 29.49 29.5 ... 30.02 30.02
  * lon                 (lon) float64 528B -95.52 -95.51 -95.5 ... -94.98 -94.98
Data variables:
    precip_incremental  (time, lat, lon) float32 2MB ...
    precip_cumulative   (time, lat, lon) float32 2MB ...
Attributes:
    Conventions:  CF-1.8
    title:        ABM Hyetograph Grid - 100-yr 24-hr Storm
    institution:  Generated by ras-commander AbmHyetographGrid
    source:       NOAA Atlas 14 CONUS NetCDF + centroid DDF ratios (AbmHyetog...
    history:      Created 2026-07-07 21:38:35 UTC
    comment:      Alternating Block Method temporal distribution. ARI=100yr, ...
    references:   Chow, V.T., Maidment, D.R., Mays, L.W. (1988). Applied Hydr...

Attributes:
  Conventions: CF-1.8
  title: ABM Hyetograph Grid - 100-yr 24-hr Storm
  institution: Generated by ras-commander AbmHyetographGrid
  source: NOAA Atlas 14 CONUS NetCDF + centroid DDF ratios (AbmHyetographGrid)
  history: Created 2026-07-07 21:38:35 UTC
  comment: Alternating Block Method temporal distribution. ARI=100yr, duration=24.0hr, timestep=15min, [email protected]%. Grid shape: 66lat x 66lon x 96timesteps.
  references: Chow, V.T., Maidment, D.R., Mays, L.W. (1988). Applied Hydrology. McGraw-Hill, Section 14.4. | NOAA Atlas 14 Precipitation Frequency Atlas of the United States. https://hdsc.nws.noaa.gov/pfds/
Python
# Basic statistics on the incremental depth field
incr = ds['precip_incremental'].values  # shape: (time, lat, lon)
n_time, n_lat, n_lon = incr.shape

print(f"Grid dimensions: {n_lat} lat x {n_lon} lon")
print(f"Time steps: {n_time} ({TIMESTEP_MIN}-min intervals = {n_time * TIMESTEP_MIN / 60:.1f} hr)")
print(f"Lat range: {ds.lat.values.min():.4f} to {ds.lat.values.max():.4f} deg N")
print(f"Lon range: {ds.lon.values.min():.4f} to {ds.lon.values.max():.4f} deg E")
print()

# Per-pixel storm total (cumulative at last timestep)
cumul = ds['precip_cumulative'].values  # shape: (time, lat, lon)
totals = cumul[-1, :, :]               # last timestep = storm total

print(f"Storm total depth across grid:")
print(f"  Min : {np.nanmin(totals):.3f} inches")
print(f"  Max : {np.nanmax(totals):.3f} inches")
print(f"  Mean: {np.nanmean(totals):.3f} inches")
print(f"  Range: {np.nanmax(totals) - np.nanmin(totals):.3f} inches  "
      f"({(np.nanmax(totals) - np.nanmin(totals)) / np.nanmin(totals) * 100:.1f}%)")

ds.close()
Text Only
Grid dimensions: 66 lat x 66 lon
Time steps: 96 (15-min intervals = 24.0 hr)
Lat range: 29.4833 to 30.0250 deg N
Lon range: -95.5167 to -94.9750 deg E

Storm total depth across grid:
  Min : 16.320 inches
  Max : 18.230 inches
  Mean: 17.447 inches
  Range: 1.910 inches  (11.7%)

2. Visualize the Grid

Two views: 1. Spatial map — storm total depth across the domain (reflects Atlas 14 spatial gradient) 2. Sample pixel hyetograph — incremental depth time series at the domain centroid

Python
import matplotlib.pyplot as plt
import xarray as xr
import numpy as np

ds = xr.open_dataset(nc_path, decode_timedelta=False)
lats = ds.lat.values
lons = ds.lon.values
cumul = ds['precip_cumulative'].values
incr  = ds['precip_incremental'].values

totals = cumul[-1, :, :]  # storm total at each pixel

# --- Plot 1: Spatial storm total ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

ax = axes[0]
im = ax.imshow(
    totals,
    origin='lower',
    extent=[lons.min(), lons.max(), lats.min(), lats.max()],
    cmap='Blues',
    aspect='auto',
)
plt.colorbar(im, ax=ax, label='Storm Total (inches)')
ax.set_title(f'{ARI_YEARS}-yr {STORM_DURATION_HR:.0f}-hr Storm Total Depth', fontweight='bold')
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')

# Mark centroid
clat = lats[len(lats) // 2]
clon = lons[len(lons) // 2]
ax.plot(clon, clat, 'r+', markersize=12, markeredgewidth=2, label='Sample pixel')
ax.legend(fontsize=9)

# --- Plot 2: Sample pixel hyetograph ---
lat_idx = len(lats) // 2
lon_idx = len(lons) // 2
pixel_incr  = incr[:, lat_idx, lon_idx]
pixel_cumul = cumul[:, lat_idx, lon_idx]
time_hr = np.arange(len(pixel_incr)) * TIMESTEP_MIN / 60.0

ax2 = axes[1]
ax2.bar(time_hr, pixel_incr, width=TIMESTEP_MIN / 60.0 * 0.9,
        color='steelblue', alpha=0.8, label='Incremental')
ax2b = ax2.twinx()
ax2b.plot(time_hr, pixel_cumul, 'r-', linewidth=1.5, label='Cumulative')
ax2b.set_ylabel('Cumulative Depth (inches)', color='r')
ax2b.tick_params(axis='y', colors='r')

ax2.set_xlabel('Time (hours)')
ax2.set_ylabel('Incremental Depth (inches / 15-min)')
ax2.set_title(f'ABM Hyetograph - Centroid Pixel ({clat:.3f}N, {clon:.3f}E)', fontweight='bold')

lines1, labels1 = ax2.get_legend_handles_labels()
lines2, labels2 = ax2b.get_legend_handles_labels()
ax2.legend(lines1 + lines2, labels1 + labels2, fontsize=9)

print(f"Centroid pixel total: {pixel_cumul[-1]:.3f} inches")
print(f"Peak interval: {pixel_incr.max():.4f} inches at t={time_hr[pixel_incr.argmax()]:.2f} hr "
      f"({pixel_incr.argmax() / len(pixel_incr) * 100:.1f}% of storm)")

plt.tight_layout()
plt.show()
ds.close()
Text Only
Centroid pixel total: 17.440 inches
Peak interval: 2.6323 inches at t=11.75 hr (49.0% of storm)

png

3. QC Verification with verify_pixel()

Compare a pixel's storm total against the NOAA PFDS point DDF value at the same location. This confirms that depth conservation is maintained through the ABM process.

Python
# QC: verify depth conservation for a pixel (internal consistency check)
# Compares sum(incremental) vs cumulative[-1] from the same NetCDF —
# no external data source needed; validates ABM rearrangement + float32 encoding.

check_lat = (BOUNDS[1] + BOUNDS[3]) / 2.0
check_lon = (BOUNDS[0] + BOUNDS[2]) / 2.0

qc = AbmHyetographGrid.verify_pixel(
    netcdf_path=nc_path,
    lat=check_lat,
    lon=check_lon,
    tolerance_pct=0.1,
)

print("QC Result (internal consistency):")
print(f"  Pixel grid location: ({qc['lat_actual']:.4f}N, {qc['lon_actual']:.4f}E)")
print(f"  Pixel storm total:   {qc['pixel_total_in']:.4f} inches  (sum of incremental)")
print(f"  NetCDF reference:    {qc['reference_depth_in']:.4f} inches  (cumulative[-1])")
print(f"  Error:               {qc['error_pct']:.4f}%")
print(f"  Status: {'PASS' if qc['passed'] else 'FAIL'} (tolerance: 0.1%)")
Text Only
2026-07-07 17:38:36 - ras_commander.precip.AbmHyetographGrid - INFO - QC PASS: pixel (29.7500N, -95.2500E) total=17.4200in, ref=17.4200in, error=0.0000%


QC Result (internal consistency):
  Pixel grid location: (29.7500N, -95.2500E)
  Pixel storm total:   17.4200 inches  (sum of incremental)
  NetCDF reference:    17.4200 inches  (cumulative[-1])
  Error:               0.0000%
  Status: PASS (tolerance: 0.1%)

Example 2 — HEC-RAS Project Bounds (Bald Eagle Creek)

For an existing HEC-RAS rain-on-grid model, derive the bounding box directly from the 2D flow area in the geometry HDF file. This ensures the ABM grid exactly covers the model domain without manually specifying coordinates.

Workflow: 1. Extract a HEC-RAS example project with a 2D flow area 2. Read the 2D area extent from the geometry HDF 3. Generate an ABM grid clipped to that extent

Python
from ras_commander import RasExamples, init_ras_project
from ras_commander.precip import Atlas14Grid

# Extract example project
project_path = RasExamples.extract_project("BaldEagleCrkMulti2D", suffix="abm_grid")
print(f"Project: {project_path}")

ras = init_ras_project(project_path, ras_version="6.6")

# Find geometry HDF
geom_hdfs = sorted(project_path.glob("*.g*.hdf"))
geom_hdf = geom_hdfs[0]
print(f"Geometry HDF: {geom_hdf.name}")
Text Only
2026-07-07 17:38:38 - ras_commander.RasExamples - INFO - Successfully extracted project 'BaldEagleCrkMulti2D' to example_projects\BaldEagleCrkMulti2D_abm_grid


Project: G:\GH\ras-commander-pr224-update\examples\example_projects\BaldEagleCrkMulti2D_abm_grid


2026-07-07 17:38:38 - ras_commander.RasPrj - INFO - ras-commander v0.98.2 | An open-source project of CLB Engineering Corporation (https://clbengineering.com/) | Docs: https://rascommander.info | GitHub: https://github.com/gpt-cmdr/ras-commander


2026-07-07 17:38:38 - ras_commander.RasPrj - INFO - Project initialized: BaldEagleDamBrk


2026-07-07 17:38:38 - ras_commander.RasPrj - INFO - Using HEC-RAS version 6.6


2026-07-07 17:38:38 - ras_commander.RasPrj - INFO - 
═══════════════════════════════════════════════════════════════════════
ras-commander | HEC-RAS Automation Library
Docs: https://rascommander.info/
Repo: https://github.com/gpt-cmdr/ras-commander
LLM agents: https://rascommander.info/llms.txt
═══════════════════════════════════════════════════════════════════════

PROJECT DATAFRAMES (single source of truth — use these, not file globbing):
  ras.plan_df        Plans, HDF paths, geometry/flow associations
  ras.geom_df        Geometry files and HDF preprocessor paths
  ras.flow_df        Steady flow files
  ras.unsteady_df    Unsteady flow files and configurations
  ras.boundaries_df  Boundary conditions (type, name, location)
  ras.results_df     Lightweight HDF results summaries
  ras.rasmap_df      RASMapper layers, terrain, land cover paths

KEY APIS (static classes — call directly, never instantiate):
  Execution:    RasCmdr.compute_plan() / compute_parallel() / compute_test_mode()
  Plan Files:   RasPlan.clone_plan() / clone_geom() / set_geom()
  Unsteady:     RasUnsteady — IC/BC management, gate openings, precipitation
  Geometry:     GeomCrossSection, GeomBridge, GeomStorage, GeomLateral, GeomMesh
  HDF Results:  HdfResultsPlan.get_wse() / get_compute_messages()
                HdfResultsMesh.get_mesh_max_ws() / get_mesh_cells_timeseries()
                HdfMesh.get_mesh_cell_points()
  QA/QC:        RasCheck.run_check() / RasFixit (geometry repair)
  DSS:          RasDss.get_timeseries() / check_pathname()
  USGS:         UsgsGaugeSpatial, GaugeMatcher, RasUsgsBoundaryGeneration
  Precipitation: StormGenerator, Atlas14Storm, PrecipAorc, Atlas14Variance
  Terrain:      RasTerrain.create_terrain_hdf() / RasTerrainMod

MULTI-PROJECT: Pass ras_object= to all API calls when using local RasPrj instances.

EXAMPLES: 100+ notebooks in examples/ (100s=execution, 200s=geometry, 300s=unsteady,
  400s=HDF results, 500s=remote, 800s=QA/QC, 900s=data integration).
  Review relevant notebooks before assembling new workflows.

PLATFORM: Most HEC-RAS operations require Windows. Linux/Wine support for
  headless execution, data access, geometry modification, and preprocessing
  is available via RasProcess (HEC-RAS 6.6+). See ras_commander/RasProcess.py.
  Remote distributed execution: ras_commander/remote/ (PsExec, Docker, SSH, cloud).
═══════════════════════════════════════════════════════════════════════


Geometry HDF: BaldEagleDamBrk.g01.hdf
Python
# Read the 2D flow area extent from the geometry HDF
# Atlas14Grid.get_pfe_from_project() returns 'bounds' which we can reuse directly
pfe_meta = Atlas14Grid.get_pfe_from_project(
    geom_hdf=geom_hdf,
    extent_source="2d_flow_area",
    durations=[24],
    return_periods=[100],
    buffer_percent=5.0,
    ras_object=ras,
)

project_bounds = pfe_meta['bounds']  # (west, south, east, north)
print(f"2D flow area bounds (with 5% buffer):")
print(f"  West:  {project_bounds[0]:.4f}")
print(f"  South: {project_bounds[1]:.4f}")
print(f"  East:  {project_bounds[2]:.4f}")
print(f"  North: {project_bounds[3]:.4f}")
if 'mesh_area_names' in pfe_meta:
    print(f"  2D Areas: {pfe_meta['mesh_area_names']}")
Text Only
2026-07-07 17:38:38 - ras_commander.precip.Atlas14Grid - INFO - Extracting extent from G:\GH\ras-commander-pr224-update\examples\example_projects\BaldEagleCrkMulti2D_abm_grid\BaldEagleDamBrk.g01.hdf using 2d_flow_area


2026-07-07 17:38:38 - ras_commander.precip.Atlas14Grid - INFO - 2D flow area bounds: W=-77.6324, S=41.0459, E=-77.3275, N=41.1842


2026-07-07 17:38:38 - ras_commander.precip.Atlas14Grid - INFO - Extracting Atlas 14 data for bounds (-77.6476, 41.0390, -77.3123, 41.1911): lat[2045:2063], lon[5683:5723]


2026-07-07 17:38:53 - ras_commander.precip.Atlas14Grid - INFO - Data transfer: 12.7 KB (vs 379.4 MB full grid, 100.0% reduction)


2D flow area bounds (with 5% buffer):
  West:  -77.6476
  South: 41.0390
  East:  -77.3123
  North: 41.1911
  2D Areas: ['BaldEagleCr']
Python
# Generate ABM grid using the project bounds
OUTPUT_NC_PROJECT = project_path / "abm_100yr_24hr.nc"

nc_path_proj = AbmHyetographGrid.generate(
    bounds=project_bounds,
    ari_years=100,
    storm_duration_hours=24.0,
    timestep_minutes=15,
    peak_position_percent=50.0,
    output_netcdf=OUTPUT_NC_PROJECT,
)

print(f"Output: {nc_path_proj}")
print(f"File size: {nc_path_proj.stat().st_size / 1024:.1f} KB")
Text Only
2026-07-07 17:38:53 - ras_commander.precip.AbmHyetographGrid - INFO - Generating ABM grid: ARI=100yr, duration=24.0hr, dt=15min, [email protected]%, bounds=(np.float64(-77.64762794494717), np.float64(41.0390203886453), np.float64(-77.31228199622441), np.float64(41.191085225880514))


2026-07-07 17:38:53 - ras_commander.precip.Atlas14Grid - INFO - Extracting Atlas 14 data for bounds (-77.6644, 41.0314, -77.2955, 41.1987): lat[2044:2064], lon[5681:5725]


2026-07-07 17:39:20 - ras_commander.precip.Atlas14Grid - INFO - Data transfer: 108.3 KB (vs 2655.6 MB full grid, 100.0% reduction)


2026-07-07 17:39:20 - ras_commander.precip.StormGenerator - INFO - Downloading Atlas 14 data for (41.115052807262906, -77.4799549705858)...


2026-07-07 17:39:21 - ras_commander.precip.StormGenerator - INFO - Downloaded Atlas 14 data for region: Ohio River Basin


2026-07-07 17:39:21 - ras_commander.precip.AbmHyetographGrid - INFO - ABM grid complete → G:\GH\ras-commander-pr224-update\examples\example_projects\BaldEagleCrkMulti2D_abm_grid\abm_100yr_24hr.nc


Output: G:\GH\ras-commander-pr224-update\examples\example_projects\BaldEagleCrkMulti2D_abm_grid\abm_100yr_24hr.nc
File size: 393.3 KB
Python
import xarray as xr
import matplotlib.pyplot as plt
import numpy as np

ds2 = xr.open_dataset(nc_path_proj, decode_timedelta=False)
lats2 = ds2.lat.values
lons2 = ds2.lon.values
totals2 = ds2['precip_cumulative'].values[-1, :, :]
incr2   = ds2['precip_incremental'].values

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Spatial total
ax = axes[0]
im = ax.imshow(
    totals2,
    origin='lower',
    extent=[lons2.min(), lons2.max(), lats2.min(), lats2.max()],
    cmap='Blues',
    aspect='auto',
)
plt.colorbar(im, ax=ax, label='Storm Total (inches)')
ax.set_title('100-yr 24-hr Storm Total - Bald Eagle Creek', fontweight='bold')
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')

print(f"Grid dimensions: {len(lats2)} lat x {len(lons2)} lon")
print(f"Storm total range: {np.nanmin(totals2):.3f} - {np.nanmax(totals2):.3f} inches")

# Centroid pixel hyetograph
li = len(lats2) // 2
lj = len(lons2) // 2
px_incr  = incr2[:, li, lj]
px_cumul = ds2['precip_cumulative'].values[:, li, lj]
time_hr2 = np.arange(len(px_incr)) * 15 / 60.0

ax2 = axes[1]
ax2.bar(time_hr2, px_incr, width=15/60*0.9, color='steelblue', alpha=0.8, label='Incremental')
ax2b = ax2.twinx()
ax2b.plot(time_hr2, px_cumul, 'r-', linewidth=1.5, label='Cumulative')
ax2b.set_ylabel('Cumulative Depth (inches)', color='r')
ax2b.tick_params(axis='y', colors='r')
ax2.set_xlabel('Time (hours)')
ax2.set_ylabel('Incremental Depth (inches / 15-min)')
ax2.set_title(f'ABM Hyetograph - Centroid Pixel ({lats2[li]:.3f}N, {lons2[lj]:.3f}E)', fontweight='bold')
lines1, labels1 = ax2.get_legend_handles_labels()
lines2, labels2 = ax2b.get_legend_handles_labels()
ax2.legend(lines1 + lines2, labels1 + labels2, fontsize=9)

plt.tight_layout()
plt.show()
ds2.close()
Text Only
Grid dimensions: 20 lat x 44 lon
Storm total range: 5.600 - 6.950 inches

png

4. Vary Peak Position

The peak_position_percent parameter controls where the largest block is placed. Common engineering choices:

Position Typical use
25% Early-peaking storm
50% Centered (default)
67% Late-peaking (common for design storms)

Compare hyetographs at three peak positions for the centroid pixel.

Python
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr

peak_positions = [25.0, 50.0, 67.0]
colors = ['#2166ac', '#4dac26', '#d7191c']
results_by_peak = {}

for peak_pct in peak_positions:
    out = AbmHyetographGrid.generate(
        bounds=BOUNDS,
        ari_years=ARI_YEARS,
        storm_duration_hours=STORM_DURATION_HR,
        timestep_minutes=TIMESTEP_MIN,
        peak_position_percent=peak_pct,
        output_netcdf=f"abm_peak{int(peak_pct)}pct.nc",
    )
    ds_tmp = xr.open_dataset(out, decode_timedelta=False)
    li = ds_tmp.lat.shape[0] // 2
    lj = ds_tmp.lon.shape[0] // 2
    results_by_peak[peak_pct] = {
        'incr':  ds_tmp['precip_incremental'].values[:, li, lj],
        'cumul': ds_tmp['precip_cumulative'].values[:, li, lj],
    }
    ds_tmp.close()

time_hr = np.arange(int(STORM_DURATION_HR * 60 / TIMESTEP_MIN)) * TIMESTEP_MIN / 60.0

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

ax_incr, ax_cumul = axes
for (peak_pct, data), color in zip(results_by_peak.items(), colors):
    ax_incr.bar(time_hr, data['incr'], width=TIMESTEP_MIN/60*0.9,
                color=color, alpha=0.6, label=f'{peak_pct:.0f}%')
    ax_cumul.plot(time_hr, data['cumul'], color=color, linewidth=2,
                  label=f'{peak_pct:.0f}%')

ax_incr.set_xlabel('Time (hours)')
ax_incr.set_ylabel('Incremental Depth (inches / 15-min)')
ax_incr.set_title('Incremental Depth by Peak Position', fontweight='bold')
ax_incr.legend(title='Peak position', fontsize=9)

ax_cumul.set_xlabel('Time (hours)')
ax_cumul.set_ylabel('Cumulative Depth (inches)')
ax_cumul.set_title('Cumulative Depth by Peak Position', fontweight='bold')
ax_cumul.legend(title='Peak position', fontsize=9)
ax_cumul.grid(True, alpha=0.3)

plt.suptitle(f'{ARI_YEARS}-yr {STORM_DURATION_HR:.0f}-hr ABM Hyetograph - Centroid Pixel',
             fontsize=12, fontweight='bold')
plt.tight_layout()
plt.show()
Text Only
2026-07-07 17:39:22 - ras_commander.precip.AbmHyetographGrid - INFO - Generating ABM grid: ARI=100yr, duration=24.0hr, dt=15min, [email protected]%, bounds=(-95.5, 29.5, -95.0, 30.0)


2026-07-07 17:39:22 - ras_commander.precip.Atlas14Grid - INFO - Extracting Atlas 14 data for bounds (-95.5250, 29.4750, -94.9750, 30.0250): lat[658:724], lon[3538:3604]


2026-07-07 17:39:37 - ras_commander.precip.Atlas14Grid - INFO - Data transfer: 536.0 KB (vs 2655.6 MB full grid, 100.0% reduction)


2026-07-07 17:39:37 - ras_commander.precip.StormGenerator - INFO - Downloading Atlas 14 data for (29.75, -95.25)...


2026-07-07 17:39:38 - ras_commander.precip.StormGenerator - INFO - Downloaded Atlas 14 data for region: Texas


2026-07-07 17:39:38 - ras_commander.precip.AbmHyetographGrid - INFO - ABM grid complete → G:\GH\ras-commander-pr224-update\examples\abm_peak25pct.nc


2026-07-07 17:39:38 - ras_commander.precip.AbmHyetographGrid - INFO - Generating ABM grid: ARI=100yr, duration=24.0hr, dt=15min, [email protected]%, bounds=(-95.5, 29.5, -95.0, 30.0)


2026-07-07 17:39:38 - ras_commander.precip.Atlas14Grid - INFO - Extracting Atlas 14 data for bounds (-95.5250, 29.4750, -94.9750, 30.0250): lat[658:724], lon[3538:3604]


2026-07-07 17:39:53 - ras_commander.precip.Atlas14Grid - INFO - Data transfer: 536.0 KB (vs 2655.6 MB full grid, 100.0% reduction)


2026-07-07 17:39:53 - ras_commander.precip.StormGenerator - INFO - Downloading Atlas 14 data for (29.75, -95.25)...


2026-07-07 17:39:54 - ras_commander.precip.StormGenerator - INFO - Downloaded Atlas 14 data for region: Texas


2026-07-07 17:39:54 - ras_commander.precip.AbmHyetographGrid - INFO - ABM grid complete → G:\GH\ras-commander-pr224-update\examples\abm_peak50pct.nc


2026-07-07 17:39:54 - ras_commander.precip.AbmHyetographGrid - INFO - Generating ABM grid: ARI=100yr, duration=24.0hr, dt=15min, [email protected]%, bounds=(-95.5, 29.5, -95.0, 30.0)


2026-07-07 17:39:54 - ras_commander.precip.Atlas14Grid - INFO - Extracting Atlas 14 data for bounds (-95.5250, 29.4750, -94.9750, 30.0250): lat[658:724], lon[3538:3604]


2026-07-07 17:40:16 - ras_commander.precip.Atlas14Grid - INFO - Data transfer: 536.0 KB (vs 2655.6 MB full grid, 100.0% reduction)


2026-07-07 17:40:16 - ras_commander.precip.StormGenerator - INFO - Downloading Atlas 14 data for (29.75, -95.25)...


2026-07-07 17:40:18 - ras_commander.precip.StormGenerator - INFO - Downloaded Atlas 14 data for region: Texas


2026-07-07 17:40:18 - ras_commander.precip.AbmHyetographGrid - INFO - ABM grid complete → G:\GH\ras-commander-pr224-update\examples\abm_peak67pct.nc

png

5. Alternative: generate_from_asc_files()

If you have already downloaded NOAA Atlas 14 ESRI ASCII raster (.asc) files from the NOAA Precipitation Frequency Data Server, use generate_from_asc_files() instead. This is useful when:

  • You need sub-hourly depth grids (5-min, 10-min, 15-min, 30-min) at full spatial resolution rather than centroid DDF ratios
  • You want full control over the input depth grids (e.g., manually adjusted values)
  • You are working offline or from a cached copy of Atlas 14 data

How to get .asc files from NOAA: 1. Go to https://hdsc.nws.noaa.gov/pfds/pfds_gis.html 2. Select your state and return period 3. Download the ESRI ASCII grid for each required duration

Raw values in NOAA .asc files are in hundredths of inchesscale_factor=0.01 (default) converts them to inches.

Python
# Example: generate_from_asc_files() API
# (Requires pre-downloaded .asc files — comment out if files not available)

# from pathlib import Path
# from ras_commander.precip import AbmHyetographGrid
#
# asc_dir = Path("atlas14_asc_files")  # folder containing downloaded .asc files
#
# asc_files = {
#     5/60:  asc_dir / "tx100yr05m.asc",   # 5-min
#     10/60: asc_dir / "tx100yr10m.asc",   # 10-min
#     15/60: asc_dir / "tx100yr15m.asc",   # 15-min
#     30/60: asc_dir / "tx100yr30m.asc",   # 30-min
#     1.0:   asc_dir / "tx100yr01h.asc",   # 1-hr
#     2.0:   asc_dir / "tx100yr02h.asc",   # 2-hr
#     3.0:   asc_dir / "tx100yr03h.asc",   # 3-hr
#     6.0:   asc_dir / "tx100yr06h.asc",   # 6-hr
#     12.0:  asc_dir / "tx100yr12h.asc",   # 12-hr
#     24.0:  asc_dir / "tx100yr24h.asc",   # 24-hr
# }
#
# nc_path_asc = AbmHyetographGrid.generate_from_asc_files(
#     asc_files=asc_files,
#     ari_years=100,
#     storm_duration_hours=24.0,
#     timestep_minutes=15,
#     peak_position_percent=50.0,
#     output_netcdf="abm_100yr_24hr_from_asc.nc",
#     scale_factor=0.01,  # NOAA .asc files are in hundredths of inches
# )
# print(f"Output: {nc_path_asc}")

print("generate_from_asc_files() example is commented out.")
print("Download .asc files from https://hdsc.nws.noaa.gov/pfds/pfds_gis.html and update paths above.")
Text Only
generate_from_asc_files() example is commented out.
Download .asc files from https://hdsc.nws.noaa.gov/pfds/pfds_gis.html and update paths above.

6. Cleanup

Python
import os

# Remove generated NetCDF files
for f in [
    "abm_houston_100yr_24hr.nc",
    "abm_peak25pct.nc",
    "abm_peak50pct.nc",
    "abm_peak67pct.nc",
]:
    p = Path(f)
    if p.exists():
        p.unlink()
        print(f"Removed {f}")

# Optionally remove extracted example project
# import shutil
# shutil.rmtree(project_path.parent, ignore_errors=True)
print("Done.")
Text Only
Removed abm_houston_100yr_24hr.nc
Removed abm_peak25pct.nc
Removed abm_peak50pct.nc
Removed abm_peak67pct.nc
Done.

Summary

This notebook demonstrated two end-to-end workflows for generating gridded ABM hyetographs:

Example 1 — Direct Bounds (generate): - Provide a bounding box, ARI, duration, timestep, and peak position - Atlas 14 spatial depth data is fetched automatically via HTTP byte-range - Sub-hourly intervals are filled via centroid DDF ratios - Output: CF-1.8 NetCDF with precip_incremental and precip_cumulative

Example 2 — HEC-RAS Project Bounds: - Use Atlas14Grid.get_pfe_from_project() to read the 2D flow area extent - Pass the returned bounds tuple directly to AbmHyetographGrid.generate() - Grid is automatically sized to the model domain

Key Parameters: - ari_years — return period: 2, 5, 10, 25, 50, 100, 200, 500, 1000 - storm_duration_hours — total storm duration (>= 1 hr for auto-download mode) - timestep_minutes — must evenly divide storm duration - peak_position_percent — 0–100; 50% = centered, 67% = late-peaking

Alternative Workflow (generate_from_asc_files): - For full sub-hourly spatial resolution, supply pre-downloaded NOAA .asc grids - Useful for offline workflows or when exact spatial sub-hourly data is required

References: - Chow, V.T., Maidment, D.R., Mays, L.W. (1988). Applied Hydrology. McGraw-Hill. - NOAA Atlas 14 - NOAA GIS Data Downloads