Operational Forecast Cycling¶
Operational forecast cycling is the process of running a hydraulic model repeatedly as new forecast data arrives, typically updating every 6 hours as HRRR (High-Resolution Rapid Refresh) weather model cycles are released.
This notebook demonstrates how to use ras-commander to automate this workflow:
- Download successive HRRR forecast cycles
- Update HEC-RAS plan simulation dates to match each forecast window
- Execute the model for each cycle with force_rerun=True
- Archive results by cycle for comparison
- Compare how forecasts evolve as new data arrives
This pattern is common in flood forecasting operations, emergency management support, and real-time hydrologic monitoring.
# =============================================================================
# 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
from ras_commander.precip import PrecipHrrr
from ras_commander.boundaries import CoastalBoundary
from ras_commander import init_ras_project, RasExamples, RasPlan, RasCmdr
What You'll Learn¶
- Set up a forecast cycling configuration for multiple HRRR cycles
- Update HEC-RAS plan simulation dates to match each forecast window using
RasPlan.update_simulation_date() - Use
force_rerun=TruewithRasCmdr.compute_plan()to ensure fresh execution each cycle - Handle data availability and latency in an operational setting
- Archive results by cycle for audit trails and comparison
- Compare sequential forecasts to assess convergence and uncertainty
Example 1: 24-Hour Retrospective (3 Cycles)¶
This example processes three consecutive HRRR forecast cycles from a single day, showing how the forecast evolves over time. This retrospective approach is useful for validating the cycling workflow before deploying operationally.
Configuration¶
# Forecast cycling configuration
# In practice, these would be set for your specific project
config = {
"project_path": "/path/to/ras_project",
"ras_version": "6.6",
"plan_number": "01",
"forecast_hours": 18,
"basin_bounds": (-77.9, 40.8, -77.3, 41.1), # Bald Eagle Creek, PA
"archive_dir": Path("forecast_archive"),
"cycles_to_process": 3,
}
# Define 3 forecast cycles (retrospective example)
base_date = datetime(2024, 7, 15)
cycles = [
{"date": base_date.strftime("%Y%m%d"), "cycle": 0, "label": "T-18h (00z)"},
{"date": base_date.strftime("%Y%m%d"), "cycle": 6, "label": "T-12h (06z)"},
{"date": base_date.strftime("%Y%m%d"), "cycle": 12, "label": "T-6h (12z)"},
]
print("Forecast Cycling Configuration:")
print(f" Project: {config['project_path']}")
print(f" Forecast hours: {config['forecast_hours']}")
print(f" Basin bounds: {config['basin_bounds']}")
print(f" Cycles to process: {config['cycles_to_process']}")
print()
print("Forecast Cycles:")
for i, cycle in enumerate(cycles, 1):
start = datetime.strptime(cycle['date'], "%Y%m%d").replace(hour=cycle['cycle'])
end = start + timedelta(hours=config['forecast_hours'])
print(f" Cycle {i} ({cycle['label']}): {start} -> {end}")
Processing Forecast Cycles¶
# Process each forecast cycle
results_summary = []
for i, cycle_config in enumerate(cycles, 1):
print(f"\n{'='*60}")
print(f"CYCLE {i}: {cycle_config['label']}")
print(f"{'='*60}")
cycle_start = datetime.strptime(
cycle_config['date'], "%Y%m%d"
).replace(hour=cycle_config['cycle'])
cycle_end = cycle_start + timedelta(hours=config['forecast_hours'])
# Step 1: Download HRRR forecast
print(f"\n Step 1: Download HRRR ({cycle_config['date']} {cycle_config['cycle']:02d}z)")
cycle_dir = config['archive_dir'] / f"cycle_{i:02d}_{cycle_config['cycle']:02d}z"
print(f" PrecipHrrr.download_forecast(")
print(f" output_dir='{cycle_dir}',")
print(f" date='{cycle_config['date']}',")
print(f" cycle={cycle_config['cycle']},")
print(f" hours={config['forecast_hours']}")
print(f" )")
# Step 2: Update plan simulation dates
print(f"\n Step 2: Update plan dates")
print(f" Start: {cycle_start.strftime('%d%b%Y %H%M').upper()}")
print(f" End: {cycle_end.strftime('%d%b%Y %H%M').upper()}")
print(f" from datetime import datetime")
print(f" RasPlan.update_simulation_date(")
print(f" '{config['plan_number']}',")
print(f" start_date=datetime({cycle_start.year}, {cycle_start.month}, {cycle_start.day}, {cycle_start.hour}, {cycle_start.minute}),")
print(f" end_date=datetime({cycle_end.year}, {cycle_end.month}, {cycle_end.day}, {cycle_end.hour}, {cycle_end.minute})")
print(f" )")
# Step 3: Execute HEC-RAS
print(f"\n Step 3: Execute HEC-RAS")
print(f" RasCmdr.compute_plan('{config['plan_number']}', force_rerun=True)")
# Step 4: Archive results
print(f"\n Step 4: Archive results to {cycle_dir}")
# Record summary (simulated)
results_summary.append({
"cycle": i,
"label": cycle_config['label'],
"start": cycle_start,
"end": cycle_end,
"status": "COMPLETED",
"peak_wse_ft": 825.0 + np.random.uniform(-2, 5), # Simulated
})
print(f"\n{'='*60}")
print(f"ALL CYCLES COMPLETE")
print(f"{'='*60}")
Comparing Forecast Results¶
# Compare peak WSE across forecast cycles
summary_df = pd.DataFrame(results_summary)
print("Forecast Cycle Comparison:")
print("=" * 70)
print(f"{'Cycle':<8} {'Label':<15} {'Start':<20} {'Peak WSE (ft)':<15} {'Status'}")
print("-" * 70)
for _, row in summary_df.iterrows():
print(f"{row['cycle']:<8} {row['label']:<15} {row['start'].strftime('%Y-%m-%d %H:%M'):<20} {row['peak_wse_ft']:<15.2f} {row['status']}")
print()
print(f"WSE Range: {summary_df['peak_wse_ft'].min():.2f} to {summary_df['peak_wse_ft'].max():.2f} ft")
print(f"WSE Spread: {summary_df['peak_wse_ft'].max() - summary_df['peak_wse_ft'].min():.2f} ft")
print()
print("Interpretation:")
print(" - Later cycles (more recent data) generally more accurate")
print(" - Decreasing spread indicates convergence on final result")
print(" - Large changes between cycles suggest high forecast uncertainty")
Example 2: Automated Pipeline Template¶
This example provides a reusable function template for operational forecast cycling. The function encapsulates the full cycle workflow and can be called repeatedly as new forecast data becomes available.
Configurable Forecast Pipeline¶
def run_forecast_cycle(project_path, ras_version, plan_number,
forecast_date, forecast_cycle, forecast_hours,
archive_dir, basin_bounds=None, coastal_point=None):
"""
Run a single forecast cycle (template function).
This function demonstrates the pattern for operational forecasting.
Adapt for your specific project.
Args:
project_path: Path to HEC-RAS project
ras_version: HEC-RAS version string (e.g., "6.6")
plan_number: Plan number to execute
forecast_date: Date string (YYYYMMDD)
forecast_cycle: HRRR cycle hour (0, 6, 12, 18)
forecast_hours: Number of forecast hours
archive_dir: Directory for archiving results
basin_bounds: Optional (west, south, east, north) for precip extraction
coastal_point: Optional (lat, lon) for coastal BC
Returns:
dict: Cycle results summary
"""
cycle_start = datetime.strptime(forecast_date, "%Y%m%d").replace(hour=forecast_cycle)
cycle_end = cycle_start + timedelta(hours=forecast_hours)
cycle_label = f"{forecast_date}_{forecast_cycle:02d}z"
result = {
"cycle_label": cycle_label,
"start": cycle_start,
"end": cycle_end,
"status": "PENDING"
}
try:
# 1. Download HRRR precipitation
hrrr_dir = archive_dir / cycle_label / "hrrr"
# grib_files = PrecipHrrr.download_forecast(
# output_dir=hrrr_dir,
# date=forecast_date,
# cycle=forecast_cycle,
# hours=forecast_hours
# )
# 2. Optional: Download coastal BC
if coastal_point:
stofs_dir = archive_dir / cycle_label / "stofs3d"
# stofs_files = CoastalBoundary.download_stofs3d(stofs_dir)
# wse_df = CoastalBoundary.extract_wse_at_point(
# stofs_dir, coastal_point[0], coastal_point[1]
# )
# CoastalBoundary.generate_stage_bc(
# wse_df, f"{project_path}/project.u{plan_number}",
# "Downstream"
# )
pass
# 3. Update plan dates
# RasPlan.update_simulation_date(
# plan_number,
# start_date=datetime(cycle_start.year, cycle_start.month, cycle_start.day,
# cycle_start.hour, cycle_start.minute),
# end_date=datetime(cycle_end.year, cycle_end.month, cycle_end.day,
# cycle_end.hour, cycle_end.minute)
# )
# 4. Execute HEC-RAS
# init_ras_project(project_path, ras_version)
# RasCmdr.compute_plan(plan_number, force_rerun=True)
# 5. Archive results
results_dir = archive_dir / cycle_label / "results"
# shutil.copytree(project_path, results_dir)
result["status"] = "COMPLETED"
except Exception as e:
result["status"] = f"FAILED: {e}"
return result
# Show the template
print("Forecast Pipeline Template Function:")
print(" run_forecast_cycle(")
print(" project_path='/models/flood_forecast',")
print(" ras_version='6.6',")
print(" plan_number='01',")
print(" forecast_date='20240715',")
print(" forecast_cycle=12,")
print(" forecast_hours=18,")
print(" archive_dir=Path('forecast_archive'),")
print(" basin_bounds=(-77.9, 40.8, -77.3, 41.1),")
print(" coastal_point=(29.35, -94.77) # Optional")
print(" )")
Running Multiple Cycles¶
# Run multiple cycles sequentially
archive_dir = Path("forecast_archive")
# Define cycles for a 24-hour period
cycles_to_run = [
{"date": "20240715", "cycle": 0},
{"date": "20240715", "cycle": 6},
{"date": "20240715", "cycle": 12},
{"date": "20240715", "cycle": 18},
]
print("Operational Forecast Schedule:")
print("=" * 55)
all_results = []
for cycle_config in cycles_to_run:
result = run_forecast_cycle(
project_path="/models/flood_forecast",
ras_version="6.6",
plan_number="01",
forecast_date=cycle_config["date"],
forecast_cycle=cycle_config["cycle"],
forecast_hours=18,
archive_dir=archive_dir,
)
all_results.append(result)
print(f" {result['cycle_label']}: {result['status']}")
print()
print(f"Completed: {sum(1 for r in all_results if r['status'] == 'COMPLETED')}/{len(all_results)} cycles")
Smart Skip for Unchanged Inputs¶
ras-commander's smart skip feature (force_rerun) is important in forecast cycling. Each new forecast cycle brings new precipitation data, so you always want fresh execution.
print("Smart Skip in Forecast Cycling:")
print("=" * 50)
print()
print("ras-commander's smart skip feature helps with forecast cycling:")
print()
print(" # Default behavior - skips if results are current")
print(" RasCmdr.compute_plan('01')")
print(" # -> 'Skipping plan 01: Results are current'")
print()
print(" # Force re-run for each new forecast cycle")
print(" RasCmdr.compute_plan('01', force_rerun=True)")
print(" # -> Always executes regardless of existing results")
print()
print("When to use each:")
print(" - force_rerun=True: Each forecast cycle (new data)")
print(" - force_rerun=False: Re-running analysis on existing results")
print(" - skip_existing=True: Simple existence check (legacy)")
Key Takeaways¶
Summary¶
Operational forecast cycling with ras-commander:
- Download new data each cycle (HRRR, STOFS-3D, USGS)
- Update plan dates to match forecast window using
RasPlan.update_simulation_date() - Execute with
force_rerun=True(new data each cycle requires fresh execution) - Archive results by cycle for comparison and audit trails
- Compare forecasts to assess convergence and uncertainty
Best Practices¶
- Use
force_rerun=Truefor each new cycle (precipitation data has changed) - Archive results with cycle-stamped directories before the next cycle overwrites them
- Monitor forecast convergence across cycles to assess uncertainty
- Handle data latency - HRRR is typically available ~2 hours after cycle time; use
max_lookback_hourswithPrecipHrrr.get_latest_forecast()to find the most recent available cycle - Set
max_lookback_hoursto tolerate occasional data delivery delays
File Organization Pattern¶
forecast_archive/
├── 20240715_00z/
│ ├── hrrr/ # HRRR GRIB2 files
│ ├── stofs3d/ # STOFS-3D NetCDF files
│ └── results/ # HEC-RAS project copy with results
├── 20240715_06z/
│ ├── hrrr/
│ └── results/
├── 20240715_12z/
│ ├── hrrr/
│ └── results/
└── summary.csv # Cycle comparison table
Adapting for Your Project¶
- Replace
project_pathwith your HEC-RAS project location - Set
basin_boundsto your watershed extent (west, south, east, north) - Add
coastal_pointif your model has a tidal downstream boundary - Adjust
forecast_hoursbased on your needs (18 for short-range, 48 for extended) - Choose cycle frequency - every 6 hours is standard for HRRR-driven operations