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}")
Text Only
Project:      BaldEagleCrkMulti2D
Geometry HDF: BaldEagleDamBrk.g09.hdf

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}")
Text Only
Land Cover datasets:
  Calibration Table: present
  Attributes: present
  Polygon Info: present
  Polygon Parts: present
  Polygon Points: present

Land Cover classes (17):
   0. 
   1. NoData
   2. Barren Land Rock/Sand/Clay
   3. Cultivated Crops
   4. Deciduous Forest
   5. Developed, High Intensity
   6. Developed, Low Intensity
   7. Developed, Medium Intensity
   8. Developed, Open Space
   9. Emergent Herbaceous Wetlands
  10. Evergreen Forest
  11. Grassland/Herbaceous
  12. Mixed Forest
  13. Open Water
  14. Pasture/Hay
  15. Shrub/Scrub
  16. Woody Wetlands

Regions: 1
Polygon points: 556
Infiltration group present: False

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}")
Text Only
Backup: BaldEagleDamBrk.g09.hdf.bak

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))
Text Only
Land cover classes: 17
Soil groups: 5 ['NoData', 'D', 'C', 'B', 'A']
Expected rows: 17 x 5 = 85
Created rows:  85

All values initialized to sentinel (-9999.0):
Land Cover Name Curve Number Abstraction Ratio Minimum Infiltration Rate
0 -9999.0 -9999.0 -9999.0
1 : D -9999.0 -9999.0 -9999.0
2 : C -9999.0 -9999.0 -9999.0
3 : B -9999.0 -9999.0 -9999.0
4 : A -9999.0 -9999.0 -9999.0
5 NoData : NoData -9999.0 -9999.0 -9999.0
6 NoData : D -9999.0 -9999.0 -9999.0
7 NoData : C -9999.0 -9999.0 -9999.0
8 NoData : B -9999.0 -9999.0 -9999.0
9 NoData : A -9999.0 -9999.0 -9999.0

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}")
Text Only
Datasets and subgroups:
  Attributes: dataset (1,) [('Name', 'S30')]
  Base Overrides: dataset (85,) [('Land Cover Name', 'S39'), ('Curve Number', '<f4'), ('Abstraction Ratio', '<f4'), ('Minimum Infiltration Rate', '<f4')]
  Polygon Info: dataset (1, 4) int32
  Polygon Parts: dataset (1, 2) int32
  Polygon Points: dataset (556, 2) float64
  Variables: group

Polygon Info attrs:
  Column = [b'Point Starting Index' b'Point Count' b'Part Starting Index'
 b'Part Count']
  Feature Type = b'Polygon'
  Row = b'Feature'

Polygon Points attrs:
  Column = [b'X' b'Y']
  Row = b'Points'

Variables subgroup datasets:
  Abstraction Ratio: shape=(1,), fields=85
  Curve Number: shape=(1,), fields=85
  Minimum Infiltration Rate: shape=(1,), fields=85

Base Overrides dtype: [('Land Cover Name', 'S39'), ('Curve Number', '<f4'), ('Abstraction Ratio', '<f4'), ('Minimum Infiltration Rate', '<f4')]
Base Overrides shape: (85,)

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))
Text Only
Rows read back: 85
All numeric columns are sentinel (-9999.0): True
Land Cover Name Curve Number Abstraction Ratio Minimum Infiltration Rate
0 -9999.0 -9999.0 -9999.0
1 : D -9999.0 -9999.0 -9999.0
2 : C -9999.0 -9999.0 -9999.0
3 : B -9999.0 -9999.0 -9999.0
4 : A -9999.0 -9999.0 -9999.0
5 NoData : NoData -9999.0 -9999.0 -9999.0
6 NoData : D -9999.0 -9999.0 -9999.0
7 NoData : C -9999.0 -9999.0 -9999.0
8 NoData : B -9999.0 -9999.0 -9999.0
9 NoData : A -9999.0 -9999.0 -9999.0

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))
Text Only
Rows with CN overrides: 12
Land Cover Name Curve Number
0 Deciduous Forest : D 79.0
1 Deciduous Forest : C 73.0
2 Deciduous Forest : B 60.0
3 Deciduous Forest : A 36.0
4 Developed, Low Intensity : D 86.0
5 Developed, Low Intensity : C 81.0
6 Developed, Low Intensity : B 72.0
7 Developed, Low Intensity : A 57.0
8 Developed, Open Space : D 84.0
9 Developed, Open Space : C 79.0
10 Developed, Open Space : B 69.0
11 Developed, Open Space : A 49.0

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.")
Text Only
Written 85 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.")
Land Cover Name expected actual match
0 Developed, Open Space : A 49.0 49.0 True
1 Developed, Open Space : B 69.0 69.0 True
2 Developed, Open Space : C 79.0 79.0 True
3 Developed, Open Space : D 84.0 84.0 True
4 Developed, Low Intensity : A 57.0 57.0 True
5 Developed, Low Intensity : B 72.0 72.0 True
6 Developed, Low Intensity : C 81.0 81.0 True
7 Developed, Low Intensity : D 86.0 86.0 True
8 Deciduous Forest : A 36.0 36.0 True
9 Deciduous Forest : B 60.0 60.0 True
10 Deciduous Forest : C 73.0 73.0 True
11 Deciduous Forest : D 79.0 79.0 True
Text Only
All 12 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
Text Only
Unchanged rows: 73
  Curve Number all sentinel:              True
  Abstraction Ratio all sentinel:          True
  Minimum Infiltration Rate all sentinel:  True

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}")
Text Only
Expected error: Infiltration group already exists in <workspace>\examples\example_projects\BaldEagleCrkMulti2D\BaldEagleDamBrk.g09.hdf. Use set_infiltration_baseoverrides() to modify values.

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.")
Text Only
Infiltration group after restore: False
Land Cover group after restore:   True

Backup 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.