Skip to content

Cloud Optimized GeoTIFF Results Export with ras2cng

Generate result rasters (WSE, Depth, Velocity, Froude, Shear Stress, DxV) from HEC-RAS simulations, convert to Cloud Optimized GeoTIFF (COG), and optionally build raster PMTiles — all using ras2cng and its generate_result_maps() function backed by RasStoreMapHelper.exe.

Python
#!pip install --upgrade ras-commander ras2cng rasterio matplotlib
Python
USE_LOCAL_SOURCE = False

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")

from ras_commander import RasExamples, init_ras_project, RasCmdr, ras
from pathlib import Path
import numpy as np

import ras_commander
print(f"ras-commander: {ras_commander.__file__}")

import ras2cng
print(f"ras2cng: {ras2cng.__file__}")

Prerequisites

  • ras-commander installed: pip install ras-commander
  • ras2cng installed: pip install ras2cng
  • HEC-RAS installed (Windows): RasStoreMapHelper.exe generates the rasters
  • rasterio installed: pip install rasterio (for raster inspection and display)
  • matplotlib installed: pip install matplotlib (for raster visualization)
  • Optional: GDAL CLI for COG conversion (gdal_translate)

What You'll Learn

  • Generate result rasters from completed HEC-RAS simulations
  • Produce WSE, Depth, Velocity, Froude, Shear Stress, and DxV raster layers
  • Convert rasters to Cloud Optimized GeoTIFF (COG) format
  • Inspect and display raster results in the notebook
  • Understand the 11 available map types and when to use each

How It Works

ras2cng's generate_result_maps() function wraps RasProcess.store_maps(), which deploys HEC-RAS's native RasStoreMapHelper.exe to render simulation results to GeoTIFF rasters. This is the same engine HEC-RAS uses internally — output matches native HEC-RAS exactly.

  • 960_cloud_native_geometry_export.ipynb — Export geometry to GeoParquet (no HEC-RAS required)
  • 961_cloud_native_results_export.ipynb — Export vector results to GeoParquet with DuckDB
  • 601_floodplain_mapping_rasprocess.ipynb — RasProcess.store_maps() directly (lower-level API)

Parameters

Python
PROJECT_NAME = "BaldEagleCrkMulti2D"
RAS_VERSION = "6.6"
OUTPUT_DIR = Path("out/962_cloud_native_cog").resolve()
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

Available Map Types

ras2cng supports 11 result raster types via the generate_result_maps() function:

Map Type Parameter Default Description
Water Surface Elevation wse On Absolute water surface elevation
Depth depth On Water depth above ground
Velocity velocity On Flow velocity magnitude
Froude Number froude Off Subcritical/supercritical classification
Shear Stress shear_stress Off Bed shear stress
Depth x Velocity depth_x_velocity Off Hazard index (DxV)
Depth x Velocity² depth_x_velocity_sq Off Force proxy (DxV²)
Inundation Boundary inundation_boundary Off Flood extent polygon (shapefile)
Arrival Time arrival_time Off Time water first arrives
Duration duration Off Time area stays wet
Recession recession Off Time water recedes

WSE, Depth, and Velocity are enabled by default. Toggle others on/off as needed.


Example 1: Core Result Rasters (WSE, Depth, Velocity)

Extract a project, run a simulation, then generate the three default result rasters as Cloud Optimized GeoTIFFs.

Extract and Initialize Project

Python
project_path = RasExamples.extract_project(PROJECT_NAME)
ras_obj = init_ras_project(project_path, RAS_VERSION)

print(f"Project: {project_path}")
print(f"Plans: {len(ras.plan_df)}")
print(f"Geometries: {len(ras.geom_df)}")
ras.plan_df[['plan_number', 'Plan Title', 'Geom File']].head()

Run Simulation

Execute the first plan to generate results HDF files. Smart skip avoids re-running if results are current.

Python
plan_number = ras.plan_df['plan_number'].iloc[0]
print(f"Running plan {plan_number}...")

try:
    RasCmdr.compute_plan(plan_number)
    print(f"Plan {plan_number} completed.")
except Exception as e:
    print(f"Execution failed: {e}")
    print("Continuing with any existing HDF files...")
Python
plan_hdfs = sorted(project_path.glob("*.p??.hdf"))
print(f"Plan HDF files: {[f.name for f in plan_hdfs]}")

if not plan_hdfs:
    print("\nNo plan HDF files found. HEC-RAS must run successfully to generate result rasters.")
else:
    for hdf in plan_hdfs:
        print(f"  {hdf.name}: {hdf.stat().st_size / 1e6:.1f} MB")

Generate Result Rasters

Use generate_result_maps() to produce WSE, Depth, and Velocity rasters with COG conversion enabled. Under the hood this calls RasProcess.store_maps() with RasStoreMapHelper.exe.

Python
from ras2cng import generate_result_maps, MapResult

core_output = OUTPUT_DIR / "core_maps"

if plan_hdfs:
    results = generate_result_maps(
        project_path,
        core_output,
        plans=[f"p{plan_number}"],
        profile="Max",
        wse=True,
        depth=True,
        velocity=True,
        ras_version=RAS_VERSION,
        convert_cog=True,
    )

    print(f"\nGenerated {len(results)} plan result(s):")
    for r in results:
        print(f"  Plan {r.plan_id}:")
        for map_type, paths in r.map_types.items():
            for p in paths:
                print(f"    {map_type}: {p.name} ({p.stat().st_size / 1e6:.1f} MB)")
        if r.errors:
            print(f"    Errors: {r.errors}")
else:
    print("No HDF files available — skipping raster generation.")
    results = []

Inspect Raster Metadata

Use rasterio to inspect the generated rasters — CRS, dimensions, bounds, and data type.

Python
try:
    import rasterio
    has_rasterio = True
except ImportError:
    has_rasterio = False
    print("rasterio not installed. Install with: pip install rasterio")

if has_rasterio and results:
    for r in results:
        for map_type, paths in r.map_types.items():
            for tif_path in paths:
                with rasterio.open(tif_path) as src:
                    print(f"\n{map_type}: {tif_path.name}")
                    print(f"  CRS:        {src.crs}")
                    print(f"  Dimensions: {src.width} x {src.height}")
                    print(f"  Bounds:     {src.bounds}")
                    print(f"  Resolution: {src.res[0]:.2f} x {src.res[1]:.2f}")
                    print(f"  Data type:  {src.dtypes[0]}")
                    print(f"  NoData:     {src.nodata}")

                    data = src.read(1, masked=True)
                    valid = data.compressed()
                    if len(valid) > 0:
                        print(f"  Value range: {valid.min():.2f}{valid.max():.2f}")
                        print(f"  Valid cells: {len(valid):,} / {data.size:,} ({100*len(valid)/data.size:.1f}%)")

Display Raster Results

Plot each result raster with matplotlib. Depth uses a warm color ramp; WSE and Velocity use sequential blue and viridis palettes.

Python
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize

CMAP_CONFIG = {
    "wse": {"cmap": "Blues", "label": "Water Surface Elevation (ft)"},
    "depth": {"cmap": "YlOrRd", "label": "Depth (ft)"},
    "velocity": {"cmap": "viridis", "label": "Velocity (ft/s)"},
}

if has_rasterio and results:
    for r in results:
        n_maps = len(r.map_types)
        if n_maps == 0:
            continue

        fig, axes = plt.subplots(1, n_maps, figsize=(6 * n_maps, 5))
        if n_maps == 1:
            axes = [axes]

        for ax, (map_type, paths) in zip(axes, r.map_types.items()):
            tif_path = paths[0]
            with rasterio.open(tif_path) as src:
                data = src.read(1, masked=True)

            cfg = CMAP_CONFIG.get(map_type, {"cmap": "viridis", "label": map_type})
            im = ax.imshow(data, cmap=cfg["cmap"])
            ax.set_title(cfg["label"])
            ax.set_axis_off()
            plt.colorbar(im, ax=ax, shrink=0.7, pad=0.02)

        fig.suptitle(f"Plan {r.plan_id} — Max Profile", fontsize=14)
        plt.tight_layout()
        plt.show()
elif not results:
    print("No results to display.")
else:
    print("Install rasterio + matplotlib for raster visualization.")

Example 2: Extended Map Types with WGS84 Reprojection

Generate additional map types (Froude, Shear Stress, Depth x Velocity) and reproject to WGS84 for web-compatible rasters.

Python
extended_output = OUTPUT_DIR / "extended_maps"

if plan_hdfs:
    extended_results = generate_result_maps(
        project_path,
        extended_output,
        plans=[f"p{plan_number}"],
        profile="Max",
        wse=False,
        depth=True,
        velocity=False,
        froude=True,
        shear_stress=True,
        depth_x_velocity=True,
        min_depth=0.1,
        ras_version=RAS_VERSION,
        reproject_wgs84=True,
        convert_cog=True,
    )

    print(f"\nGenerated {len(extended_results)} plan result(s):")
    for r in extended_results:
        print(f"  Plan {r.plan_id}:")
        for map_type, paths in r.map_types.items():
            for p in paths:
                size_mb = p.stat().st_size / 1e6 if p.exists() else 0
                print(f"    {map_type}: {p.name} ({size_mb:.1f} MB)")
        if r.errors:
            print(f"    Errors: {r.errors}")
else:
    print("No HDF files available — skipping extended raster generation.")
    extended_results = []

Inspect Extended Rasters

Verify the extended rasters have WGS84 CRS and the depth threshold was applied.

Python
if has_rasterio and extended_results:
    for r in extended_results:
        for map_type, paths in r.map_types.items():
            for tif_path in paths:
                if not tif_path.exists():
                    continue
                with rasterio.open(tif_path) as src:
                    print(f"\n{map_type}: {tif_path.name}")
                    print(f"  CRS: {src.crs}")
                    print(f"  Dimensions: {src.width} x {src.height}")

                    data = src.read(1, masked=True)
                    valid = data.compressed()
                    if len(valid) > 0:
                        print(f"  Value range: {valid.min():.3f}{valid.max():.3f}")
                        print(f"  Valid cells: {len(valid):,}")

                        if map_type == "depth":
                            below_threshold = (valid < 0.1).sum()
                            print(f"  Cells below min_depth (0.1): {below_threshold}")

Display Extended Results

Python
EXTENDED_CMAP = {
    "depth": {"cmap": "YlOrRd", "label": "Depth (ft) — min_depth=0.1"},
    "froude": {"cmap": "RdYlGn_r", "label": "Froude Number"},
    "shear_stress": {"cmap": "magma", "label": "Shear Stress (lb/ft²)"},
    "depth_x_velocity": {"cmap": "inferno", "label": "Depth × Velocity (ft²/s)"},
}

if has_rasterio and extended_results:
    for r in extended_results:
        n_maps = len(r.map_types)
        if n_maps == 0:
            continue

        fig, axes = plt.subplots(1, n_maps, figsize=(5 * n_maps, 5))
        if n_maps == 1:
            axes = [axes]

        for ax, (map_type, paths) in zip(axes, r.map_types.items()):
            tif_path = paths[0]
            if not tif_path.exists():
                ax.text(0.5, 0.5, f"{map_type}\nnot generated", ha="center", va="center")
                continue
            with rasterio.open(tif_path) as src:
                data = src.read(1, masked=True)

            cfg = EXTENDED_CMAP.get(map_type, {"cmap": "viridis", "label": map_type})
            im = ax.imshow(data, cmap=cfg["cmap"])
            ax.set_title(cfg["label"], fontsize=10)
            ax.set_axis_off()
            plt.colorbar(im, ax=ax, shrink=0.7, pad=0.02)

        fig.suptitle(f"Plan {r.plan_id} — Extended Map Types (WGS84)", fontsize=14)
        plt.tight_layout()
        plt.show()
elif not extended_results:
    print("No extended results to display.")

Understanding Cloud Optimized GeoTIFF (COG)

Cloud Optimized GeoTIFF is a regular GeoTIFF with internal tiling and overview pyramids that enable efficient HTTP range requests. This means a web client can fetch just the tiles it needs without downloading the entire file.

When convert_cog=True: 1. generate_result_maps() first creates standard GeoTIFFs via RasStoreMapHelper.exe 2. Then converts each to COG using gdal_translate -of COG 3. Output files get a _cog suffix (e.g., Depth_Max_cog.tif)

Requirements: GDAL must be installed and gdal_translate on PATH.

Benefits of COG: - Stream rasters from S3/Azure/GCS without full download - Built-in overviews for fast zoom-out rendering - Compatible with QGIS, ArcGIS, leafmap, TiTiler, and other tools - No special server needed — works from any HTTP host


Output Summary

Python
total_size = 0
file_count = 0

print("Output files:")
for f in sorted(OUTPUT_DIR.rglob("*")):
    if f.is_file():
        size_kb = f.stat().st_size / 1e3
        total_size += size_kb
        file_count += 1
        rel = f.relative_to(OUTPUT_DIR)
        print(f"  {str(rel):50s} {size_kb:>8.1f} KB")

print(f"\nTotal: {file_count} files, {total_size / 1e3:.1f} MB")

Key Takeaways

  1. generate_result_maps() wraps RasStoreMapHelper.exe — output matches native HEC-RAS raster generation exactly
  2. 11 map types available — WSE, Depth, Velocity enabled by default; toggle Froude, Shear, DxV, and timing variables as needed
  3. COG conversion via convert_cog=True — produces web-streamable rasters with tiling and overviews
  4. WGS84 reprojection via reproject_wgs84=True — makes rasters compatible with web mapping tools
  5. Depth threshold via min_depth=0.1 — filters shallow cells to NoData for cleaner flood maps
  6. MapResult dataclass provides structured output with plan_id, map_types dict, and errors list

Python API Quick Reference

Python
from ras2cng import generate_result_maps

# Core rasters with COG conversion
results = generate_result_maps(
    project_path, output_dir,
    wse=True, depth=True, velocity=True,
    convert_cog=True,
)

# Extended types with WGS84 + depth filter
results = generate_result_maps(
    project_path, output_dir,
    froude=True, shear_stress=True, depth_x_velocity=True,
    min_depth=0.1, reproject_wgs84=True, convert_cog=True,
)

# Access results
for r in results:
    print(r.plan_id, r.map_types.keys(), r.errors)

CLI Quick Reference

Bash
# Default: WSE + Depth + Velocity
ras2cng map path/to/project/ ./maps/

# All core types with COG conversion
ras2cng map path/to/project/ ./maps/ --cog

# Extended types with depth filter and WGS84
ras2cng map path/to/project/ ./maps/ \
  --froude --shear-stress --dv \
  --min-depth 0.1 --wgs84 --cog

# Specific plan only
ras2cng map path/to/project/ ./maps/ --plans p01 --cog

Next Steps

  • 960_cloud_native_geometry_export.ipynb — Export geometry to GeoParquet (no HEC-RAS required)
  • 961_cloud_native_results_export.ipynb — Vector results export with DuckDB analytics
  • 601_floodplain_mapping_rasprocess.ipynb — Lower-level RasProcess.store_maps() API
  • ras2cng documentation — Complete API and CLI reference
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.