HMS-RAS Coupled Forecast Execution¶
This notebook demonstrates the pattern for coupling HEC-HMS and HEC-RAS in a forecast workflow. HEC-HMS (Hydrologic Modeling System) computes rainfall-runoff to produce flow hydrographs, which are then fed as upstream boundary conditions into a HEC-RAS hydraulic model.
Requirements:
- ras-commander — HEC-RAS automation (this library)
- hms-commander — HEC-HMS automation (separate package, optional)
When hms-commander is not installed, this notebook shows the API call patterns so you can implement the coupling once you have both packages available.
Typical use cases: - Real-time flood forecasting - Design storm inundation mapping - Scenario analysis (existing vs. future conditions) - Climate change impact assessment
# =============================================================================
# DEVELOPMENT MODE TOGGLE
# =============================================================================
USE_LOCAL_SOURCE = True # <-- 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__}")
from pathlib import Path
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# ras-commander imports
from ras_commander import init_ras_project, RasCmdr, RasPlan
# hms-commander imports (optional - for coupled workflows)
try:
from hms_commander import init_hms_project, HmsCmdr, HmsControl, HmsResults
HMS_AVAILABLE = True
print("hms-commander available - coupled workflow enabled")
except ImportError:
HMS_AVAILABLE = False
print("hms-commander not installed - showing pattern only")
print("Install with: pip install hms-commander")
What You'll Learn¶
- Configure an HMS forecast run using
HmsControl.set_time_window() - Execute HMS headless via
HmsCmdr.compute_run() - Extract outlet hydrographs from HMS DSS output using
HmsResults.get_outflow_timeseries() - Feed HMS flow output as a RAS upstream boundary condition
- Align time windows between HMS and RAS
- Execute the RAS hydraulic model with updated boundary conditions
The HMS-RAS Coupling Pattern¶
HMS and RAS are separate programs with separate project files. The coupling happens in Python:
- HMS receives precipitation and computes runoff (rainfall-runoff transformation)
- HMS writes flow hydrographs to a DSS file
- Python reads the HMS DSS output
- Python writes the flow data as a RAS boundary condition (DSS or inline)
- RAS executes the hydraulic simulation using the HMS-derived flows
There is no direct file linkage between HMS and RAS — the connection is orchestrated in user scripts.
print("HMS-RAS Coupled Forecast Pipeline:")
print("=" * 65)
print()
print(" [Weather Data / Precipitation Forecast]")
print(" |")
print(" v")
print(" +-------------------+")
print(" | HEC-HMS | <- hms-commander")
print(" | (Rainfall-Runoff) |")
print(" +-------------------+")
print(" |")
print(" | Flow hydrograph at watershed outlet (DSS)")
print(" v")
print(" [Python Orchestration Script]")
print(" | - Read HMS DSS output")
print(" | - Write to RAS boundary condition")
print(" | - Align simulation time windows")
print(" v")
print(" +-------------------+")
print(" | HEC-RAS | <- ras-commander")
print(" | (Hydraulic Model) |")
print(" +-------------------+")
print(" |")
print(" v")
print(" [Flood Results: WSE, Depth, Velocity, Inundation Extent]")
print()
print("Key handoff: HMS outlet flow -> RAS upstream boundary condition")
Example 1: Simple HMS-RAS Handoff¶
A single HMS watershed outlet feeds a single RAS upstream boundary condition. This is the most common configuration for a simple stream reach with one contributing watershed.
Step 1: Configure HMS Forecast Dates¶
# Define forecast window
forecast_start = datetime(2024, 7, 15, 0, 0)
forecast_end = forecast_start + timedelta(hours=72)
print(f"Forecast Period: {forecast_start} to {forecast_end}")
print(f"Duration: {(forecast_end - forecast_start).total_seconds() / 3600:.0f} hours")
print()
if HMS_AVAILABLE:
# Initialize HMS project
# hms = init_hms_project("/path/to/hms_project")
# Update control spec dates
# HmsControl.set_time_window(
# start_date=forecast_start.strftime("%d%B%Y %H:%M"),
# end_date=forecast_end.strftime("%d%B%Y %H:%M")
# )
print("HMS time window would be updated via HmsControl.set_time_window()")
else:
print("Pattern (hms-commander):")
print(" hms = init_hms_project('/path/to/hms_project')")
print(f" HmsControl.set_time_window(")
print(f" start_date='{forecast_start.strftime('%d%B%Y %H:%M')}',")
print(f" end_date='{forecast_end.strftime('%d%B%Y %H:%M')}'")
print(f" )")
Step 2: Execute HMS Forecast¶
if HMS_AVAILABLE:
# Execute HMS forecast run
# HmsCmdr.compute_run("Run 1", max_memory="8G")
print("HMS execution would run via HmsCmdr.compute_run()")
else:
print("Pattern (hms-commander):")
print(" HmsCmdr.compute_run('Forecast Run', max_memory='8G')")
print()
print("HMS execution details:")
print(" - Runs HEC-HMS headless via Jython CLI")
print(" - No GUI required for automated workflows")
print(" - Configurable JVM memory allocation")
print(" - Returns execution status and log messages")
print(" - DSS output written to project results folder")
Step 3: Extract HMS Outlet Hydrograph¶
if HMS_AVAILABLE:
# Extract flow at HMS outlet
# dss_file = Path("hms_project/results.dss")
# flow_df = HmsResults.get_outflow_timeseries(dss_file, "Outlet")
print("HMS results would be extracted via HmsResults.get_outflow_timeseries()")
else:
# Create synthetic flow data for demonstration
np.random.seed(42)
times = pd.date_range(forecast_start, forecast_end, freq='1h')
# Simulate a typical flood hydrograph
n = len(times)
peak_idx = n // 3 # Peak at 1/3 of duration (rising limb steeper than recession)
t_rise = np.linspace(0, np.pi / 2, peak_idx)
t_fall = np.linspace(np.pi / 2, np.pi, n - peak_idx)
base_flow = 150.0
peak_flow = 5200.0
rising = base_flow + (peak_flow - base_flow) * np.sin(t_rise)
falling = base_flow + (peak_flow - base_flow) * np.sin(t_fall)
flows = np.concatenate([rising, falling])
# Add minor random variation to simulate real HMS output
flows = flows + np.random.normal(0, 15, n)
flows = np.maximum(flows, base_flow) # No negative flows
flow_df = pd.DataFrame({
'datetime': times,
'flow_cfs': flows
})
peak_row = flow_df.loc[flow_df['flow_cfs'].idxmax()]
print("Synthetic HMS outlet hydrograph (for demonstration):")
print(f" Time steps: {len(flow_df)}")
print(f" Start flow: {flow_df['flow_cfs'].iloc[0]:.0f} cfs")
print(f" Peak flow: {peak_row['flow_cfs']:.0f} cfs at {peak_row['datetime']}")
print(f" End flow: {flow_df['flow_cfs'].iloc[-1]:.0f} cfs")
print()
print(flow_df.head(10).to_string(index=False))
print(" ...")
Step 4: Write Hydrograph to RAS Unsteady File¶
The HMS outlet flow becomes the upstream boundary condition in HEC-RAS. There are two approaches: 1. Write directly to the RAS unsteady flow file (inline table) 2. Write to a DSS file and reference it from the RAS unsteady file
print("Approach A: Inline flow table in RAS unsteady file")
print("-" * 50)
print(" from ras_commander import RasUnsteady")
print()
print(" # hydrograph_df must have columns: 'hour' (float, hours since simulation start)")
print(" # and 'value' (flow rate in the model's flow units, e.g., cfs or cms)")
print(" hms_df = flow_df.copy()")
print(" hms_df['hour'] = (hms_df['datetime'] - forecast_start).dt.total_seconds() / 3600")
print(" hms_df = hms_df.rename(columns={'flow_cfs': 'value'})")
print()
print(" RasUnsteady.set_boundary_inline_hydrograph(")
print(" unsteady_file='project.u01',")
print(" hydrograph_df=hms_df[['hour', 'value']],")
print(" bc_type='Flow Hydrograph', # default")
print(" river='MyRiver', # optional: filter by river name")
print(" reach='MyReach', # optional: filter by reach name")
print(" station='12345', # optional: filter by station")
print(" )")
print()
print("Approach B: Write to DSS, reference from RAS unsteady file")
print("-" * 50)
print(" from ras_commander.dss import RasDss")
print()
print(" RasDss.write_timeseries(")
print(" dss_file='boundary.dss',")
print(" pathname='//BASIN/UPSTREAM/FLOW/01JUL2024/1HOUR/HMS/',")
print(" times=flow_df['datetime'].tolist(),")
print(" values=flow_df['flow_cfs'].tolist(),")
print(" units='CFS',")
print(" data_type='INST-VAL'")
print(" )")
print()
print("Recommendation:")
print(" Use DSS (Approach B) for operational forecasting — provides")
print(" audit trail of each forecast cycle's input boundary conditions.")
Step 5: Update RAS Plan Dates and Execute¶
# Update RAS plan simulation dates to match HMS forecast window
print("Update RAS plan simulation dates:")
print(f" from datetime import datetime")
print(f" RasPlan.update_simulation_date(")
print(f" '01',")
print(f" start_date=datetime({forecast_start.year}, {forecast_start.month}, {forecast_start.day}, {forecast_start.hour}, {forecast_start.minute}),")
print(f" end_date=datetime({forecast_end.year}, {forecast_end.month}, {forecast_end.day}, {forecast_end.hour}, {forecast_end.minute})")
print(f" )")
print()
print("Execute RAS hydraulic simulation:")
print(" init_ras_project('/path/to/ras_project', '6.6')")
print(" RasCmdr.compute_plan('01', force_rerun=True, num_cores=4)")
print()
print("After execution, extract results:")
print(" from ras_commander.hdf import HdfResultsMesh")
print(" max_wse = HdfResultsMesh.get_mesh_max_ws('01', ras_object=ras)")
print(" max_depth = HdfResultsMesh.get_mesh_max_depth('01', ras_object=ras)")
Example 2: Multi-Element HMS with Multiple RAS Boundaries¶
For complex watersheds, HMS models multiple sub-basins with separate outlet junctions. Each junction contributes flow at a different location in the RAS model — lateral inflows, tributary junctions, and the main upstream boundary.
# For complex watersheds, HMS may have multiple outlets
# Each becomes a separate RAS boundary condition
print("Multi-Element HMS -> Multi-Boundary RAS:")
print("=" * 55)
print()
# Simulated HMS elements with their corresponding RAS BC locations
hms_elements = {
"Junction-1": {"bc_type": "upstream", "bc_location": "Upstream_Main", "peak_cfs": 3500},
"Junction-2": {"bc_type": "lateral", "bc_location": "Lateral_Trib1", "peak_cfs": 1200},
"Junction-3": {"bc_type": "lateral", "bc_location": "Lateral_Trib2", "peak_cfs": 800},
}
total_peak = sum(v["peak_cfs"] for v in hms_elements.values())
print(f" {'HMS Element':<20} {'BC Type':<12} {'RAS BC Location':<25} {'Peak (cfs)'}")
print(f" {'-'*20} {'-'*12} {'-'*25} {'-'*10}")
for element, config in hms_elements.items():
print(f" {element:<20} {config['bc_type']:<12} {config['bc_location']:<25} {config['peak_cfs']:>10,}")
print(f" {'':20} {'':12} {'Total peak flow':<25} {total_peak:>10,}")
print()
print("Pattern for multi-element coupling:")
print()
print(" for element, config in hms_elements.items():")
print(" # Extract hydrograph from HMS DSS output")
print(" flow = HmsResults.get_outflow_timeseries(dss_file, element)")
print(" # Prepare hydrograph_df with required 'hour' and 'value' columns")
print(" hms_df = flow.rename(columns={'flow_cfs': 'value'})")
print(" hms_df['hour'] = (hms_df['datetime'] - forecast_start).dt.total_seconds() / 3600")
print(" # Write to RAS unsteady file — river/reach/station filter is optional")
print(" RasUnsteady.set_boundary_inline_hydrograph(")
print(" unsteady_file=unsteady_file,")
print(" hydrograph_df=hms_df[['hour', 'value']],")
print(" bc_type='Flow Hydrograph',")
print(" river=config.get('river'), # optional: filter by river name")
print(" reach=config.get('reach'), # optional: filter by reach name")
print(" station=config.get('station') # optional: filter by station")
print(" )")
Time Step Alignment¶
HMS and RAS may use different computation time steps. The key is ensuring the RAS boundary condition time series interval is compatible with the RAS simulation interval.
print("Time Step Alignment Considerations:")
print("=" * 60)
print()
print(f"{'HMS Output Interval':<25} {'RAS BC Interval':<20} {'Behavior'}")
print("-" * 70)
print(f"{'5-minute':<25} {'5-minute':<20} {'Direct match - ideal'}")
print(f"{'15-minute':<25} {'5-minute':<20} {'RAS interpolates BC values'}")
print(f"{'1-hour':<25} {'1-minute':<20} {'RAS interpolates BC values'}")
print(f"{'1-hour':<25} {'1-hour':<20} {'Direct match'}")
print(f"{'1-hour':<25} {'6-hour':<20} {'BC coarser than RAS - avoid'}")
print()
print("Best practices:")
print(" 1. Match HMS output interval to RAS BC interval")
print(" 2. HMS warm-up: Start HMS 6-24 hours before RAS start")
print(" (watershed routing introduces lag time)")
print(" 3. HMS end time: Run HMS through RAS end + travel time")
print(" (ensure flow at outlet covers full RAS simulation)")
print(" 4. For 2D models: 1-hour HMS output + 30-sec RAS works")
print(" (RAS interpolates between hourly BC values)")
print()
print("Typical HMS warm-up adjustment:")
print(" hms_start = forecast_start - timedelta(hours=12) # 12-hr warm-up")
print(" ras_start = forecast_start # RAS starts at actual forecast time")
Key Takeaways¶
Summary¶
The HMS-RAS coupled workflow requires coordination between two separate tools:
hms-commanderfor HMS execution and results extractionras-commanderfor RAS execution and boundary management- User orchestration script to connect the two (not library code)
Critical Integration Points¶
| Step | Tool | Method |
|---|---|---|
| Update HMS dates | hms-commander | HmsControl.set_time_window() |
| Execute HMS | hms-commander | HmsCmdr.compute_run() |
| Extract HMS flow | hms-commander | HmsResults.get_outflow_timeseries() |
| Write RAS BC | ras-commander | RasDss.write_timeseries() |
| Update RAS dates | ras-commander | RasPlan.update_simulation_date() |
| Execute RAS | ras-commander | RasCmdr.compute_plan() |
HMS DSS Pathname Convention¶
HMS DSS output pathnames follow this structure:
RAS typically expects:
You may need to remap pathnames when transferring data from HMS to RAS DSS files.
Common Pitfalls¶
- Time window mismatch — Ensure HMS outputs cover the full RAS simulation window including any travel time from watershed outlet to the RAS model domain.
- Units — HMS uses CFS or CMS; confirm RAS boundary condition expects the same units.
- DSS pathname errors — RAS is strict about DSS pathname format; validate with
RasDss.check_pathname()before execution. - Initial conditions — RAS initial conditions (IC) at simulation start should reflect baseflow, not zero flow, to avoid numerical instability.
Related Notebooks¶
310_dss_boundary_extraction.ipynb— Reading and writing DSS boundary conditions110_single_plan_execution.ipynb— Basic RAS plan execution720_precipitation_methods_comprehensive.ipynb— Generating design storm precipitation for HMS input
import shutil
# Clean up any extracted example projects created during this notebook
# (None were extracted in this demo — add paths here if you extracted projects locally)
cleanup_dirs = []
for d in cleanup_dirs:
p = Path(d) if not isinstance(d, Path) else d
if p.exists():
shutil.rmtree(p, ignore_errors=True)
print(f"Cleaned up: {d}")
print("Cleanup complete.")
print("Note: This notebook uses synthetic/pattern data only — no example projects were extracted.")