Skip to content

Infiltration Base Override Authoring

Programmatically create and modify SCS Curve Number infiltration overrides in HEC-RAS geometry HDF files — no GUI required. Demonstrates HdfInfiltration.create_infiltration_group() and HdfInfiltration.set_infiltration_baseoverrides().

Python
from pathlib import Path
import logging
import shutil

import h5py
import numpy as np
import pandas as pd
from IPython.display import display

from ras_commander import *
from ras_commander.hdf import HdfInfiltration, HdfLandCover

for logger_name in [
    'ras_commander.RasExamples',
    'ras_commander.RasMap',
    'ras_commander.RasPrj',
    'ras_commander.RasUtils',
    'ras_commander.hdf.HdfBase',
    'ras_commander.hdf.HdfInfiltration',
    'ras_commander.hdf.HdfLandCover',
]:
    logging.getLogger(logger_name).setLevel(logging.WARNING)

Setup & Project Extraction

Extract the BaldEagleCrkMulti2D example project. This project has multiple geometry files — we need one whose Land Cover (Manning's n) group includes polygon spatial datasets (Attributes, Polygon Info/Parts/Points), which are written when the Land Cover layer is configured with calibration regions in the HEC-RAS GUI. We also need no Infiltration group yet.

Python
RAS_VERSION = '7.0'

project_path = RasExamples.extract_project('BaldEagleCrkMulti2D')
init_ras_project(project_path, RAS_VERSION)

LC_GROUP = "Geometry/Land Cover (Manning's n)"
LC_REQUIRED = ['Calibration Table', 'Attributes',
               'Polygon Info', 'Polygon Parts', 'Polygon Points']
INFIL_GROUP = 'Geometry/Infiltration'

geometry_hdf_path = None
for _, row in ras.geom_df[ras.geom_df['has_2d_mesh']].iterrows():
    candidate = Path(row['hdf_path'])
    if not candidate.exists():
        continue
    with h5py.File(candidate, 'r') as hf:
        if LC_GROUP not in hf:
            continue
        lc = hf[LC_GROUP]
        if not all(ds in lc for ds in LC_REQUIRED):
            continue
        if INFIL_GROUP in hf:
            continue
        geometry_hdf_path = candidate
        break

assert geometry_hdf_path is not None, (
    "No geometry HDF found with full Land Cover polygons and no Infiltration group."
)

print(f"Project:      {project_path.name}")
print(f"Geometry HDF: {geometry_hdf_path.name}")

Verify Prerequisites

Confirm the selected geometry HDF has a fully preprocessed Land Cover group with polygon spatial datasets, and no Infiltration group yet.

Python
with h5py.File(geometry_hdf_path, 'r') as hf:
    lc_group = hf[LC_GROUP]

    print("Land Cover datasets:")
    for ds_name in LC_REQUIRED:
        present = ds_name in lc_group
        print(f"  {ds_name}: {'present' if present else 'MISSING'}")
        assert present, f"Missing {ds_name}"

    lc_cal = lc_group['Calibration Table']
    lc_names = [v.decode('utf-8').strip() for v in lc_cal['Land Cover Name']]
    print(f"\nLand Cover classes ({len(lc_names)}):")
    for i, name in enumerate(lc_names):
        print(f"  {i:2d}. {name}")

    n_regions = lc_group['Attributes'].shape[0]
    n_points = lc_group['Polygon Points'].shape[0]
    print(f"\nRegions: {n_regions}")
    print(f"Polygon points: {n_points}")
    print(f"Infiltration group present: {INFIL_GROUP in hf}")

Backup Before Editing

Back up the geometry HDF so it can be restored at the end.

Python
backup_path = Path(str(geometry_hdf_path) + '.bak')
shutil.copy2(geometry_hdf_path, backup_path)
print(f"Backup: {backup_path.name}")

Create the Infiltration Group

create_infiltration_group() reads the land cover class names from the existing Calibration Table, reuses the Land Cover polygon geometry, and builds the full /Geometry/Infiltration/ group with:

  • Attributes — region names
  • Polygon Info / Parts / Points — copied from Land Cover regions
  • Base Overrides — one row per (land cover class x soil group) combination, all initialized to -9999.0 (sentinel for "no active override")
  • Variables/ subgroup — per-parameter compound datasets

The soil groups follow USDA hydrologic soil classification: ['NoData', 'D', 'C', 'B', 'A'].

Python
bo_df = HdfInfiltration.create_infiltration_group(geometry_hdf_path)

n_classes = len(lc_names)
n_soil_groups = len(HdfInfiltration.SOIL_GROUPS)
expected_rows = n_classes * n_soil_groups

print(f"Land cover classes: {n_classes}")
print(f"Soil groups: {n_soil_groups} {HdfInfiltration.SOIL_GROUPS}")
print(f"Expected rows: {n_classes} x {n_soil_groups} = {expected_rows}")
print(f"Created rows:  {len(bo_df)}")
assert len(bo_df) == expected_rows

print(f"\nAll values initialized to sentinel (-9999.0):")
display(bo_df.head(10))

Inspect HDF Structure

Verify the created group has the expected datasets, polygon attributes, and Variables subgroup.

Python
with h5py.File(geometry_hdf_path, 'r') as hf:
    infil = hf['Geometry/Infiltration']

    print("Datasets and subgroups:")
    for key in infil:
        item = infil[key]
        kind = "group" if isinstance(item, h5py.Group) else f"dataset {item.shape} {item.dtype}"
        print(f"  {key}: {kind}")

    print("\nPolygon Info attrs:")
    for attr_name, attr_val in infil['Polygon Info'].attrs.items():
        print(f"  {attr_name} = {attr_val}")

    print("\nPolygon Points attrs:")
    for attr_name, attr_val in infil['Polygon Points'].attrs.items():
        print(f"  {attr_name} = {attr_val}")

    print("\nVariables subgroup datasets:")
    for key in infil['Variables']:
        ds = infil['Variables'][key]
        print(f"  {key}: shape={ds.shape}, fields={len(ds.dtype.names)}")

    print(f"\nBase Overrides dtype: {infil['Base Overrides'].dtype}")
    print(f"Base Overrides shape: {infil['Base Overrides'].shape}")

Read Back with the Getter

Confirm get_infiltration_baseoverrides() can read the freshly created table and that all numeric values are the -9999.0 sentinel.

Python
readback_df = HdfInfiltration.get_infiltration_baseoverrides(geometry_hdf_path)
assert readback_df is not None, "Expected non-None DataFrame from getter."

numeric_cols = ['Curve Number', 'Abstraction Ratio', 'Minimum Infiltration Rate']
for col in numeric_cols:
    assert (readback_df[col] == -9999.0).all(), f"Expected all -9999.0 in {col}"

print(f"Rows read back: {len(readback_df)}")
print(f"All numeric columns are sentinel (-9999.0): True")
display(readback_df.head(10))

Modify Curve Numbers

Assign realistic SCS Curve Numbers to a few land cover / soil group combinations. Values of -9999.0 remain as "no override" — HEC-RAS treats them as the default.

We modify the DataFrame returned by the getter, then write it back with set_infiltration_baseoverrides().

Python
CN_UPDATES = {
    'Developed, Open Space : A': 49.0,
    'Developed, Open Space : B': 69.0,
    'Developed, Open Space : C': 79.0,
    'Developed, Open Space : D': 84.0,
    'Developed, Low Intensity : A': 57.0,
    'Developed, Low Intensity : B': 72.0,
    'Developed, Low Intensity : C': 81.0,
    'Developed, Low Intensity : D': 86.0,
    'Deciduous Forest : A': 36.0,
    'Deciduous Forest : B': 60.0,
    'Deciduous Forest : C': 73.0,
    'Deciduous Forest : D': 79.0,
}

modified_df = readback_df.copy()

for name, cn in CN_UPDATES.items():
    mask = modified_df['Land Cover Name'] == name
    if mask.any():
        modified_df.loc[mask, 'Curve Number'] = cn

changed_rows = modified_df[modified_df['Curve Number'] != -9999.0]
print(f"Rows with CN overrides: {len(changed_rows)}")
display(changed_rows[['Land Cover Name', 'Curve Number']].reset_index(drop=True))

Write Modified Overrides

Call set_infiltration_baseoverrides() to write the modified DataFrame back to the HDF. The function performs in-place modification when the row count is unchanged.

Python
result_df = HdfInfiltration.set_infiltration_baseoverrides(
    geometry_hdf_path,
    modified_df,
)

assert result_df is not None, "set_infiltration_baseoverrides() returned None"
print(f"Written {len(result_df)} rows to Base Overrides.")

Round-Trip Verification

Read the Base Overrides back from the HDF and confirm every modified Curve Number matches the value we wrote.

Python
verify_df = HdfInfiltration.get_infiltration_baseoverrides(geometry_hdf_path)

verification = []
for name, expected_cn in CN_UPDATES.items():
    row = verify_df[verify_df['Land Cover Name'] == name]
    if row.empty:
        verification.append({'Land Cover Name': name, 'expected': expected_cn,
                             'actual': None, 'match': False})
    else:
        actual_cn = float(row['Curve Number'].iloc[0])
        verification.append({'Land Cover Name': name, 'expected': expected_cn,
                             'actual': actual_cn,
                             'match': np.isclose(actual_cn, expected_cn)})

verify_result = pd.DataFrame(verification)
display(verify_result)

assert verify_result['match'].all(), "Round-trip mismatch detected!"
print(f"\nAll {len(CN_UPDATES)} Curve Number overrides verified.")

Verify Unchanged Rows

Rows that were not modified should still have the sentinel value.

Python
unchanged_mask = ~verify_df['Land Cover Name'].isin(CN_UPDATES.keys())
unchanged_rows = verify_df[unchanged_mask]

unchanged_cn_sentinel = (unchanged_rows['Curve Number'] == -9999.0).all()
unchanged_ar_sentinel = (unchanged_rows['Abstraction Ratio'] == -9999.0).all()
unchanged_mir_sentinel = (unchanged_rows['Minimum Infiltration Rate'] == -9999.0).all()

print(f"Unchanged rows: {len(unchanged_rows)}")
print(f"  Curve Number all sentinel:              {unchanged_cn_sentinel}")
print(f"  Abstraction Ratio all sentinel:          {unchanged_ar_sentinel}")
print(f"  Minimum Infiltration Rate all sentinel:  {unchanged_mir_sentinel}")

assert unchanged_cn_sentinel and unchanged_ar_sentinel and unchanged_mir_sentinel

Duplicate Creation Guard

Calling create_infiltration_group() a second time on the same file should raise ValueError with a message about the group already existing.

Python
try:
    HdfInfiltration.create_infiltration_group(geometry_hdf_path)
    assert False, "Should have raised ValueError for duplicate creation."
except ValueError as e:
    assert "already exists" in str(e), f"Unexpected error: {e}"
    print(f"Expected error: {e}")

Restore from Backup

Copy the backup over the modified HDF and confirm the Infiltration group is gone — proving the backup is clean.

Python
shutil.copy2(backup_path, geometry_hdf_path)

with h5py.File(geometry_hdf_path, 'r') as hf:
    restored_has_infiltration = "Geometry/Infiltration" in hf
    restored_has_land_cover = LC_GROUP in hf

print(f"Infiltration group after restore: {restored_has_infiltration}")
print(f"Land Cover group after restore:   {restored_has_land_cover}")

assert not restored_has_infiltration, "Infiltration group should be absent after restore."
assert restored_has_land_cover, "Land Cover group should still be present."

backup_path.unlink()
print("\nBackup cleaned up. Original geometry HDF restored.")

Key Takeaways

  • create_infiltration_group() builds the full /Geometry/Infiltration/ structure from an existing Land Cover layer — no HEC-RAS GUI required.
  • The Land Cover group must be fully preprocessed (with polygon spatial datasets) before calling this function.
  • All override values start at -9999.0 (sentinel for "use default").
  • set_infiltration_baseoverrides() writes modified Curve Numbers, Abstraction Ratios, and Minimum Infiltration Rates back to the HDF.
  • The getter to modify to setter round-trip is lossless.
  • Duplicate creation is guarded by a ValueError.
  • Always back up the geometry HDF before programmatic edits.
CLB Engineering Corporation  ·  LLM Forward Engineering
RAS Commander is a free and open-source project maintained by CLB Engineering Corporation. For agencies and firms seeking to modernize H&H workflows with LLM Forward approaches, contact CLB to partner with the engineers who wrote the automation.