Skip to content

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.

Python
# =============================================================================
# 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__}")
Python
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=True with RasCmdr.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

Python
# 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

Python
# 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

Python
# 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

Python
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

Python
# 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.

Python
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:

  1. Download new data each cycle (HRRR, STOFS-3D, USGS)
  2. Update plan dates to match forecast window using RasPlan.update_simulation_date()
  3. Execute with force_rerun=True (new data each cycle requires fresh execution)
  4. Archive results by cycle for comparison and audit trails
  5. Compare forecasts to assess convergence and uncertainty

Best Practices

  • Use force_rerun=True for 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_hours with PrecipHrrr.get_latest_forecast() to find the most recent available cycle
  • Set max_lookback_hours to tolerate occasional data delivery delays

File Organization Pattern

Text Only
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

  1. Replace project_path with your HEC-RAS project location
  2. Set basin_bounds to your watershed extent (west, south, east, north)
  3. Add coastal_point if your model has a tidal downstream boundary
  4. Adjust forecast_hours based on your needs (18 for short-range, 48 for extended)
  5. Choose cycle frequency - every 6 hours is standard for HRRR-driven operations
Python
import shutil

for folder_name in ["forecast_archive"]:
    p = Path(folder_name)
    if p.exists():
        shutil.rmtree(p, ignore_errors=True)
        print(f"Cleaned up: {folder_name}")
    else:
        print(f"Not found (nothing to clean): {folder_name}")

print("Done!")
CLB Engineering Corporation  ·  LLM Forward Engineering
RAS Commander is a free and open-source project maintained by CLB Engineering Corporation. For agencies and firms seeking to modernize H&H workflows with LLM Forward approaches, contact CLB to partner with the engineers who wrote the automation.