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}")
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.
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}")
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.
backup_path = Path(str(geometry_hdf_path) + '.bak')
shutil.copy2(geometry_hdf_path, backup_path)
print(f"Backup: {backup_path.name}")
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'].
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))
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.
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}")
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.
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))
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().
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))
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.
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.")
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.
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 |
All 12 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
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.
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}")
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.
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.")
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.