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__}")
Text Only
LOCAL SOURCE MODE: Loading from <workspace>/ras_commander


Loaded: <workspace>\ras_commander\__init__.py
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}")
Text Only
Forecast Cycling Configuration:
  Project: /path/to/ras_project
  Forecast hours: 18
  Basin bounds: (-77.9, 40.8, -77.3, 41.1)
  Cycles to process: 3

Forecast Cycles:
  Cycle 1 (T-18h (00z)): 2024-07-15 00:00:00 -> 2024-07-15 18:00:00
  Cycle 2 (T-12h (06z)): 2024-07-15 06:00:00 -> 2024-07-16 00:00:00
  Cycle 3 (T-6h  (12z)): 2024-07-15 12:00:00 -> 2024-07-16 06:00:00

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}")
Text Only
============================================================
CYCLE 1: T-18h (00z)
============================================================

  Step 1: Download HRRR (20240715 00z)
    PrecipHrrr.download_forecast(
        output_dir='forecast_archive\cycle_01_00z',
        date='20240715',
        cycle=0,
        hours=18
    )

  Step 2: Update plan dates
    Start: 15JUL2024 0000
    End:   15JUL2024 1800
    from datetime import datetime
    RasPlan.update_simulation_date(
        '01',
        start_date=datetime(2024, 7, 15, 0, 0),
        end_date=datetime(2024, 7, 15, 18, 0)
    )

  Step 3: Execute HEC-RAS
    RasCmdr.compute_plan('01', force_rerun=True)

  Step 4: Archive results to forecast_archive\cycle_01_00z

============================================================
CYCLE 2: T-12h (06z)
============================================================

  Step 1: Download HRRR (20240715 06z)
    PrecipHrrr.download_forecast(
        output_dir='forecast_archive\cycle_02_06z',
        date='20240715',
        cycle=6,
        hours=18
    )

  Step 2: Update plan dates
    Start: 15JUL2024 0600
    End:   16JUL2024 0000
    from datetime import datetime
    RasPlan.update_simulation_date(
        '01',
        start_date=datetime(2024, 7, 15, 6, 0),
        end_date=datetime(2024, 7, 16, 0, 0)
    )

  Step 3: Execute HEC-RAS
    RasCmdr.compute_plan('01', force_rerun=True)

  Step 4: Archive results to forecast_archive\cycle_02_06z

============================================================
CYCLE 3: T-6h  (12z)
============================================================

  Step 1: Download HRRR (20240715 12z)
    PrecipHrrr.download_forecast(
        output_dir='forecast_archive\cycle_03_12z',
        date='20240715',
        cycle=12,
        hours=18
    )

  Step 2: Update plan dates
    Start: 15JUL2024 1200
    End:   16JUL2024 0600
    from datetime import datetime
    RasPlan.update_simulation_date(
        '01',
        start_date=datetime(2024, 7, 15, 12, 0),
        end_date=datetime(2024, 7, 16, 6, 0)
    )

  Step 3: Execute HEC-RAS
    RasCmdr.compute_plan('01', force_rerun=True)

  Step 4: Archive results to forecast_archive\cycle_03_12z

============================================================
ALL CYCLES COMPLETE
============================================================

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")
Text Only
Forecast Cycle Comparison:
======================================================================
Cycle    Label           Start                Peak WSE (ft)   Status
----------------------------------------------------------------------
1        T-18h (00z)     2024-07-15 00:00     825.75          COMPLETED
2        T-12h (06z)     2024-07-15 06:00     826.51          COMPLETED
3        T-6h  (12z)     2024-07-15 12:00     825.37          COMPLETED

WSE Range: 825.37 to 826.51 ft
WSE Spread: 1.14 ft

Interpretation:
  - Later cycles (more recent data) generally more accurate
  - Decreasing spread indicates convergence on final result
  - 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("  )")
Text Only
Forecast Pipeline Template Function:
  run_forecast_cycle(
      project_path='/models/flood_forecast',
      ras_version='6.6',
      plan_number='01',
      forecast_date='20240715',
      forecast_cycle=12,
      forecast_hours=18,
      archive_dir=Path('forecast_archive'),
      basin_bounds=(-77.9, 40.8, -77.3, 41.1),
      coastal_point=(29.35, -94.77)  # Optional
  )

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")
Text Only
Operational Forecast Schedule:
=======================================================
  20240715_00z: COMPLETED
  20240715_06z: COMPLETED
  20240715_12z: COMPLETED
  20240715_18z: COMPLETED

Completed: 4/4 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)")
Text Only
Smart Skip in Forecast Cycling:
==================================================

ras-commander's smart skip feature helps with forecast cycling:

  # Default behavior - skips if results are current
  RasCmdr.compute_plan('01')
  # -> 'Skipping plan 01: Results are current'

  # Force re-run for each new forecast cycle
  RasCmdr.compute_plan('01', force_rerun=True)
  # -> Always executes regardless of existing results

When to use each:
  - force_rerun=True: Each forecast cycle (new data)
  - force_rerun=False: Re-running analysis on existing results
  - 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!")
Text Only
Not found (nothing to clean): forecast_archive
Done!