Skip to content

Direct GeoTIFF Gridded Rain-on-Grid

This notebook authors an explicit precipitation GeoTIFF time series on the BaldEagleCrkMulti2D RasExamples mesh, translates it to a durable project-local NetCDF, validates the temporary precompute HDF, computes the plan, and plots final cell precipitation and hydraulic depth. GeoTIFF is a ras-commander ingestion format; it is not advertised as a native HEC-RAS meteorology source. Global gridded meteorology requires HEC-RAS 6.0 or later.

Python
# DEVELOPMENT MODE TOGGLE: True uses this checkout; False uses the installed package.
USE_LOCAL_SOURCE = True

import sys
from pathlib import Path

REPO_ROOT = Path.cwd()
if REPO_ROOT.name.lower() == "examples":
    REPO_ROOT = REPO_ROOT.parent
if USE_LOCAL_SOURCE and str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))
Python
from __future__ import annotations

from datetime import datetime
import os

import h5py
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import rasterio
import xarray as xr
from rasterio.transform import from_origin

from ras_commander import (
    RasCmdr, RasExamples, RasPlan, RasPreprocess, RasUnsteady, init_ras_project,
)
from ras_commander.hdf import HdfMesh, HdfResultsMesh, HdfResultsPlan
from ras_commander.results import ResultsParser

RUN_ROOT = Path(os.environ.get(
    "RAS_COMMANDER_EXAMPLE_RUN_ROOT",
    REPO_ROOT / "working" / "example_729_geotiff_rain_on_grid",
))
RAS_VERSION = os.environ.get("RAS_COMMANDER_EXAMPLE_RAS_VERSION", "6.6")
EVENT_START = datetime(2024, 8, 9, 10)
EVENT_END = datetime(2024, 8, 9, 16)
RAIN_TIMES = [datetime(2024, 8, 9, hour) for hour in (11, 12, 13)]
RUN_ROOT.mkdir(parents=True, exist_ok=True)

Check the selected HEC-RAS release

Python
capabilities = RasUnsteady.get_gridded_precipitation_capabilities(RAS_VERSION)
capabilities.require_global_gridded()
display(pd.DataFrame([{
    "version": capabilities.version,
    "native_sources": ", ".join(capabilities.native_sources),
    "ras_commander_inputs": ", ".join(capabilities.ras_commander_inputs),
    "geotiff_qualification": capabilities.qualification_for("geotiff"),
    "timing": capabilities.period_average_timing,
}]))
version native_sources ras_commander_inputs geotiff_qualification timing
0 6.6 dss, netcdf, grib dss, netcdf, grib, geotiff qualified_windows_and_wine corrected

Extract the RasExamples project and create deterministic GeoTIFF forcing

Python
project_path = RasExamples.extract_project(
    "BaldEagleCrkMulti2D", output_path=RUN_ROOT, suffix="geotiff_729"
)
ras = init_ras_project(project_path, ras_version=RAS_VERSION, load_results_summary=False)

plan_row = None
for _, candidate in ras.plan_df.iterrows():
    candidate_u = str(candidate["unsteady_number"]).zfill(2)
    config = RasUnsteady.get_met_precipitation_config(candidate_u, ras_object=ras)
    if config.get("enabled") and config.get("mode") == "Gridded":
        plan_row = candidate
        break
if plan_row is None:
    raise RuntimeError("No enabled gridded-precipitation plan in Bald Eagle")

PLAN_NUMBER = str(plan_row["plan_number"]).zfill(2)
UNSTEADY_NUMBER = str(plan_row["unsteady_number"]).zfill(2)
GEOMETRY_NUMBER = str(plan_row["geometry_number"]).zfill(2)
GEOMETRY_HDF = ras.project_folder / f"{ras.project_name}.g{GEOMETRY_NUMBER}.hdf"
mesh_areas = HdfMesh.get_mesh_areas(GEOMETRY_HDF)
if mesh_areas.crs is None:
    raise ValueError("The geometry fixture must have a projected CRS")

west, south, east, north = (float(value) for value in mesh_areas.total_bounds)
cell_size = max(east - west, north - south) / 24.0
width = max(2, int(np.ceil((east - west) / cell_size)))
height = max(2, int(np.ceil((north - south) / cell_size)))
transform = from_origin(west, north, cell_size, cell_size)
row, column = np.mgrid[:height, :width]
rain_frames = [
    0.5 + 0.5 * (column >= width // 2),
    0.25 + 0.75 * (row >= height // 2),
    0.1 + 0.9 * ((row + column) % 3 == 0),
]

geotiff_folder = ras.project_folder / "Precipitation" / "geotiff"
geotiff_folder.mkdir(parents=True, exist_ok=True)
geotiff_paths = []
for index, values in enumerate(rain_frames, start=1):
    path = geotiff_folder / f"synthetic_precip_{index:02d}.tif"
    with rasterio.open(
        path, "w", driver="GTiff", width=width, height=height, count=1,
        dtype="float32", transform=transform, crs=mesh_areas.crs, nodata=-9999.0,
    ) as dataset:
        dataset.write(np.asarray(values, dtype=np.float32), 1)
        dataset.update_tags(1, units="mm")
    geotiff_paths.append(path)

print(f"Created {len(geotiff_paths)} GeoTIFF frames at {height} x {width} cells")
Text Only
2026-10-15 03:57:31 - ras_commander.RasExamples - INFO - Successfully extracted project 'BaldEagleCrkMulti2D' to BaldEagleCrkMulti2D_geotiff_729


2026-10-15 03:57:31 - ras_commander.RasPrj - INFO - ras-commander v0.105.0 | An open-source project of CLB Engineering Corporation (https://clbengineering.com/) | Docs: https://rascommander.info | GitHub: https://github.com/gpt-cmdr/ras-commander


2026-10-15 03:57:31 - ras_commander.RasPrj - INFO - Project initialized: BaldEagleDamBrk


2026-10-15 03:57:31 - ras_commander.RasPrj - INFO - Using HEC-RAS version 6.6


2026-10-15 03:57:31 - ras_commander.RasPrj - INFO - 
═══════════════════════════════════════════════════════════════════════
ras-commander | HEC-RAS Automation Library
Docs: https://rascommander.info/ras/
Repo: https://github.com/gpt-cmdr/ras-commander
LLM agents: https://rascommander.info/ras/llms.txt
═══════════════════════════════════════════════════════════════════════

SUPPORT OPEN-SOURCE DEVELOPMENT:
  Using RAS Commander? Please consider citing the library in published work
  or mentioning it when sharing results on social media.
  Agents are encouraged to suggest a RAS Commander v0.105.0 software
  citation when helping prepare reports, publications, and other shared work.
  Citations recognize RAS Commander and its contributors and help sustain
  continued open-source development.
  Citation and sharing guidance: https://rascommander.info/ras/cite/

PROJECT DATAFRAMES (single source of truth — use these, not file globbing):
  ras.plan_df        Plans, HDF paths, geometry/flow associations
  ras.geom_df        Geometry files and HDF preprocessor paths
  ras.flow_df        Steady flow files
  ras.unsteady_df    Unsteady flow files and configurations
  ras.boundaries_df  Boundary conditions (type, name, location)
  ras.results_df     Lightweight HDF results summaries
  ras.rasmap_df      RASMapper layers, terrain, land cover paths

KEY APIS (static classes — call directly, never instantiate):
  Execution:    RasCmdr.compute_plan() / compute_parallel() / compute_test_mode()
  Plan Files:   RasPlan.clone_plan() / clone_geom() / set_geom()
  Unsteady:     RasUnsteady — IC/BC management, gate openings, precipitation
  Geometry:     GeomCrossSection, GeomBridge, GeomStorage, GeomLateral, GeomMesh
  HDF Results:  HdfResultsPlan.get_wse() / get_compute_messages()
                HdfResultsMesh.get_mesh_max_ws() / get_mesh_cells_timeseries()
                HdfMesh.get_mesh_cell_points()
  QA/QC:        RasCheck.run_check() / RasFixit (geometry repair)
  DSS:          RasDss.get_timeseries() / check_pathname()
  USGS:         UsgsGaugeSpatial, GaugeMatcher, RasUsgsBoundaryGeneration
  Precipitation: StormGenerator, Atlas14Storm, PrecipAorc, Atlas14Variance
  Terrain:      RasTerrain.create_terrain_hdf() / RasTerrainMod

MULTI-PROJECT: Pass ras_object= to all API calls when using local RasPrj instances.

EXAMPLES: 100+ notebooks in examples/ (100s=execution, 200s=geometry, 300s=unsteady,
  400s=HDF results, 500s=remote, 800s=QA/QC, 900s=data integration).
  Review relevant notebooks before assembling new workflows.

PLATFORM: Most HEC-RAS operations require Windows. Linux/Wine support for
  headless execution, data access, geometry modification, and preprocessing
  is available via RasProcess (HEC-RAS 6.6+). See ras_commander/RasProcess.py.
  Remote distributed execution: ras_commander/remote/ (PsExec, Docker, SSH, cloud).
═══════════════════════════════════════════════════════════════════════


Created 3 GeoTIFF frames at 17 x 24 cells
Python
fig, axes = plt.subplots(1, 3, figsize=(13, 4), constrained_layout=True)
extent = [west, west + width * cell_size, north - height * cell_size, north]
for ax, values, timestamp in zip(axes, rain_frames, RAIN_TIMES):
    image = ax.imshow(values, extent=extent, origin="upper", cmap="Blues", vmin=0, vmax=1)
    mesh_areas.boundary.plot(ax=ax, color="#b84a39", linewidth=1.0)
    ax.set_title(timestamp.strftime("%Y-%m-%d %H:%M"))
    ax.set_aspect("equal")
fig.colorbar(image, ax=axes, shrink=0.8, label="Interval precipitation (mm)")
fig.suptitle("Direct GeoTIFF source frames and 2D mesh extent")
plt.show()

png

Author the durable source and validate precompute rainfall

The GeoTIFF setter preserves explicit meteorology choices and completes the HEC-RAS-required evapotranspiration, air-density, and air-pressure modes. The temporary-HDF, final-HDF, and runtime-message checks remain the authoritative validation.

Python
import_result = RasUnsteady.set_gridded_precipitation_geotiff(
    UNSTEADY_NUMBER,
    geotiff_paths,
    timestamps=RAIN_TIMES,
    units="mm",
    value_type="amount",
    first_timestep_hours=1.0,
    interpolation="Bilinear",
    ratio=1.0,
    nodata_policy="error",
    ras_object=ras,
)
assert import_result.cache_path.is_file()
assert import_result.shape == (4, height, width)
assert import_result.nodata_count == 0
print(
    "Durable NetCDF: "
    f"{import_result.cache_path.relative_to(ras.project_folder).as_posix()}"
)
print(f"Semantic content hash: {import_result.cache_hash}")

RasPlan.update_simulation_date(PLAN_NUMBER, EVENT_START, EVENT_END, ras_object=ras)
RasPlan.update_plan_intervals(
    PLAN_NUMBER, output_interval="1HOUR", instantaneous_interval="1HOUR",
    mapping_interval="1HOUR", ras_object=ras,
)
preprocess_result = RasPreprocess.preprocess_plan(PLAN_NUMBER, ras_object=ras, max_wait=600)
assert preprocess_result, preprocess_result.error
with h5py.File(preprocess_result.tmp_hdf_path, "r") as hdf:
    solver_values = hdf["Event Conditions/Meteorology/Precipitation/Values"][...]
    solver_timestamps = hdf["Event Conditions/Meteorology/Precipitation/Timestamp"][...]
assert solver_values.ndim == 2
assert solver_values.shape[0] == solver_timestamps.shape[0]
assert float(np.nanmax(solver_values)) > 0.0
Text Only
2026-10-15 03:57:32 - ras_commander.RasUnsteady - INFO - Imported gridded precipitation into BaldEagleDamBrk.u03.hdf: 4 timesteps, 408 cells, range=0.0-3.0 mm


2026-10-15 03:57:32 - ras_commander.RasUnsteady - INFO - Completed required gridded meteorology modes in BaldEagleDamBrk.u03: Air Pressure


2026-10-15 03:57:32 - ras_commander.RasUnsteady - INFO - Configured gridded precipitation in BaldEagleDamBrk.u03: source=.\Precipitation\_ras_commander_cache\precip-513ec7c96a69e80d.nc, interpolation=Bilinear, dataset=precipitation


2026-10-15 03:57:32 - ras_commander.RasPlan - INFO - Updated simulation date in plan file: BaldEagleDamBrk.p06


2026-10-15 03:57:32 - ras_commander.RasPlan - INFO - Updated intervals in plan file: BaldEagleDamBrk.p06


Durable NetCDF: Precipitation/_ras_commander_cache/precip-513ec7c96a69e80d.nc
Semantic content hash: 513ec7c96a69e80d1bd837d7f75b270374208fbd36403e2f9e03106e10f1e974


2026-10-15 03:57:32 - ras_commander.RasPreprocess - INFO - Starting HEC-RAS preprocessing for plan 06


2026-10-15 03:57:32 - ras_commander.RasBco - INFO - Monitoring BaldEagleDamBrk.bco06 for 'Starting Unsteady Flow Computations' signal...


2026-10-15 03:57:35 - ras_commander.RasPreprocess - INFO - Waiting for exact CompletePreProcess writer(s) before stopping gridded preprocessing: 24860


2026-10-15 03:57:45 - ras_commander.RasPreprocess - INFO - No CompletePreProcess writer is active and the owned unsteady solver started; stopping before it advances


2026-10-15 03:57:45 - ras_commander.RasBco - INFO - Detected an observed CompletePreProcess writer that has exited with quiescent preprocessing artifacts


2026-10-15 03:57:45 - ras_commander.RasPreprocess - INFO - Preprocessing complete for plan 06 in 12.2s: tmp.hdf=12.0MB, b=3KB, x=5KB
Python
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(np.arange(solver_values.shape[0]), np.nanmean(solver_values, axis=1), marker="o")
ax.set_title("Temporary plan-HDF solver-facing precipitation")
ax.set_xlabel("Precipitation frame")
ax.set_ylabel("Mean interval precipitation (in)")
ax.grid(True, alpha=0.25)
plt.show()

png

Compute and inspect final HDF rainfall and hydraulic response

Python
compute_result = RasCmdr.compute_plan(
    PLAN_NUMBER, ras_object=ras, force_rerun=True, verify=True,
    use_optimal_hdf_settings=True,
    hdf_output_variables=[
        "Cell Hydraulic Depth",
        "Cell Precipitation Rate",
        "Cell Cumulative Precipitation Depth",
    ],
)
assert compute_result
PLAN_HDF = ras.project_folder / f"{ras.project_name}.p{PLAN_NUMBER}.hdf"
messages = HdfResultsPlan.get_compute_messages_hdf_only(PLAN_HDF)
assert "Processing Precipitation data" in messages
parsed_messages = ResultsParser.parse_compute_messages(messages)
runtime = ResultsParser.parse_compute_messages_runtime(messages)
assert not parsed_messages["has_errors"], parsed_messages["first_error_line"]
display(pd.DataFrame([{
    "plan": f"p{PLAN_NUMBER}",
    "HEC-RAS version": RAS_VERSION,
    "completed": parsed_messages["completed"],
    "errors": parsed_messages["error_count"],
    "warnings": parsed_messages["warning_count"],
    "runtime (s)": runtime.get("runtime_complete_process_seconds"),
    "volume error (%)": runtime.get("vol_error_percent"),
}]))
precip_messages = [
    line for line in messages.splitlines()
    if any(term in line.casefold() for term in ("precip", "rain", "raster"))
]
print("\n".join(precip_messages) or "No precipitation-specific messages reported")
Text Only
2026-10-15 03:57:45 - ras_commander.RasPlan - INFO - Updated HDF write parameters in plan file: BaldEagleDamBrk.p06


2026-10-15 03:57:45 - ras_commander.RasPlan - INFO - Added HDF output variable 'Cell Hydraulic Depth' to plan file: BaldEagleDamBrk.p06


2026-10-15 03:57:45 - ras_commander.RasPlan - INFO - Added HDF output variable 'Cell Precipitation Rate' to plan file: BaldEagleDamBrk.p06


2026-10-15 03:57:45 - ras_commander.RasPlan - INFO - Added HDF output variable 'Cell Cumulative Precipitation Depth' to plan file: BaldEagleDamBrk.p06


2026-10-15 03:57:45 - ras_commander.RasCmdr - INFO - Applied 'balanced' HDF settings profile to plan: BaldEagleDamBrk.p06


2026-10-15 03:57:45 - ras_commander.ExecutionArtifacts - INFO - Removed plan 06 execution artifact: BaldEagleDamBrk.bco06
plan HEC-RAS version completed errors warnings runtime (s) volume error (%)
0 p06 6.6 True 0 0 18.0 0.000004
Text Only
Plan: 'Gridded Precip - Infiltration' (BaldEagleDamBrk.p06)
Processing Precipitation data...
Finished Processing Precipitation data (0.154s)
Python
cumulative = HdfResultsMesh.get_mesh_cells_timeseries(
    PLAN_HDF, var="Cell Cumulative Precipitation Depth"
)
rate = HdfResultsMesh.get_mesh_cells_timeseries(PLAN_HDF, var="Cell Precipitation Rate")
hydraulic = HdfResultsMesh.get_mesh_cells_timeseries(PLAN_HDF, var="Cell Hydraulic Depth")
mesh_name = next(iter(cumulative))
rain_data = cumulative[mesh_name]["Cell Cumulative Precipitation Depth"]
rate_data = rate[mesh_name]["Cell Precipitation Rate"]
depth_data = hydraulic[mesh_name]["Cell Hydraulic Depth"]
assert float(np.nanmax(rain_data.values)) > 0.0
assert float(np.nanmax(depth_data.values)) > 0.0
final_rain = np.asarray(rain_data.isel(time=-1).values, dtype=float)
final_rate = np.asarray(rate_data.isel(time=-1).values, dtype=float)
rain_steps = np.asarray(rain_data.values, dtype=float)
assert np.isfinite(final_rain).all() and np.isfinite(final_rate).all()
assert np.allclose(final_rate, 0.0, atol=1e-7)
assert float(np.nanmin(np.diff(rain_steps, axis=0))) >= -1e-7
# Physical cells only, matching the ghost-free cell results above.
mesh_cell_points = HdfMesh.get_mesh_cell_points(GEOMETRY_HDF, include_ghost_cells=False)
with xr.open_dataset(import_result.cache_path) as cache_dataset:
    cache_final_mm = cache_dataset["precipitation"].isel(time=-1)
    expected_cell_in = (
        cache_final_mm.interp(
            x=xr.DataArray(mesh_cell_points.geometry.x.to_numpy(), dims="cell_id"),
            y=xr.DataArray(mesh_cell_points.geometry.y.to_numpy(), dims="cell_id"),
            method="linear",
        ).values
        / 25.4
    )
actual_cell_in = np.asarray(
    rain_data.isel(time=-1).sel(
        cell_id=mesh_cell_points["cell_id"].to_numpy(dtype=int)
    ).values,
    dtype=float,
)
comparison_mask = (actual_cell_in > 0.0) & np.isfinite(expected_cell_in)
comparison_error_in = actual_cell_in[comparison_mask] - expected_cell_in[comparison_mask]
assert int(comparison_mask.sum()) >= int(0.85 * len(mesh_cell_points))
assert float(np.max(np.abs(comparison_error_in))) <= 1e-5
display(pd.DataFrame([{
    "source-sampled cells compared": int(comparison_mask.sum()),
    "raining HDF cells": int(np.count_nonzero(actual_cell_in > 0.0)),
    "maximum source-vs-HDF error (in)": float(np.max(np.abs(comparison_error_in))),
    "final HDF minimum cumulative depth (in)": float(final_rain.min()),
    "final HDF mean cumulative depth (in)": float(final_rain.mean()),
    "final HDF maximum cumulative depth (in)": float(final_rain.max()),
    "final HDF maximum ending rate (in/hr)": float(final_rate.max()),
}]).round(6))

rain_time = pd.to_datetime(cumulative[mesh_name]["time"].values)
depth_time = pd.to_datetime(hydraulic[mesh_name]["time"].values)
fig, axes = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)
axes[0].plot(rain_time, rain_data.mean(dim="cell_id").values, marker="o", label="mean")
axes[0].plot(rain_time, rain_data.max(dim="cell_id").values, marker="o", label="max")
axes[0].set_title("Final per-cell cumulative precipitation")
axes[0].set_ylabel("Depth (in)")
axes[1].plot(depth_time, depth_data.mean(dim="cell_id").values, marker="o", label="mean")
axes[1].plot(depth_time, depth_data.max(dim="cell_id").values, marker="o", label="max")
axes[1].set_title("Hydraulic response")
axes[1].set_ylabel("Depth (ft)")
for ax in axes:
    ax.grid(True, alpha=0.25)
    ax.legend()
    ax.tick_params(axis="x", rotation=30)
plt.show()
print("Manual review of the full HEC-RAS runtime messages and rainfall/results maps is still required.")
source-sampled cells compared raining HDF cells maximum source-vs-HDF error (in) final HDF minimum cumulative depth (in) final HDF mean cumulative depth (in) final HDF maximum cumulative depth (in) final HDF maximum ending rate (in/hr)
0 17591 18066 0.0 0.033501 0.06681 0.115746 0.0

png

Text Only
Manual review of the full HEC-RAS runtime messages and rainfall/results maps is still required.