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().
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.
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.
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.
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'].
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.
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.
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().
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.
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.
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.
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.
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.
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.