MRMS NetCDF Rain-on-Grid Validation¶
This notebook validates the direct MRMS gridded precipitation path in ras-commander: MRMS GRIB2 download, NetCDF export, RasUnsteady.set_gridded_precipitation() HDF import, HEC-RAS compute, and model-output precipitation checks on a RasExamples project.
Python
# =============================================================================
# DEVELOPMENT MODE TOGGLE
# =============================================================================
# True uses the local checkout; False uses the installed ras-commander 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:
local_path = str(REPO_ROOT)
if local_path not in sys.path:
sys.path.insert(0, local_path)
print(f"LOCAL SOURCE MODE: Loading from {REPO_ROOT / 'ras_commander'}")
else:
print("PIP PACKAGE MODE: Loading installed ras-commander")
Python
from __future__ import annotations
from datetime import datetime
import os
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from ras_commander import RasCmdr, RasExamples, RasPlan, RasUnsteady, init_ras_project
from ras_commander.hdf import HdfMesh, HdfProject, HdfResultsMesh
from ras_commander.precip import PrecipMrms
REPO_ROOT = Path.cwd()
if REPO_ROOT.name.lower() == "examples":
REPO_ROOT = REPO_ROOT.parent
RUN_ROOT = Path(
os.environ.get(
"RAS_COMMANDER_EXAMPLE_RUN_ROOT",
REPO_ROOT / "working" / "example_924_mrms_netcdf_rain_on_grid",
)
)
RUN_ROOT.mkdir(parents=True, exist_ok=True)
PROJECT_NAME = "BaldEagleCrkMulti2D"
PROJECT_SUFFIX = "mrms_netcdf_924"
RAS_VERSION = "7.0"
EVENT_START = datetime(2024, 8, 9, 10)
EVENT_LAST_QPE = datetime(2024, 8, 9, 13)
SIM_END = datetime(2024, 8, 9, 16)
MRMS_RESOLUTION_METERS = 8000.0
print(f"Run root: {RUN_ROOT}")
print(f"MRMS event: {EVENT_START} through {EVENT_LAST_QPE}")
Python
def find_gridded_precipitation_plan(ras):
"""Return the first plan already configured for gridded precipitation."""
for _, row in ras.plan_df.iterrows():
unsteady_number = str(row["unsteady_number"]).zfill(2)
config = RasUnsteady.get_met_precipitation_config(unsteady_number, ras_object=ras)
if config.get("enabled") and config.get("mode") == "Gridded":
return row, config
raise RuntimeError("No enabled gridded precipitation plan found in RasExamples project")
def get_plan_geometry_hdf(ras, plan_row: pd.Series) -> Path:
geometry_number = str(plan_row["geometry_number"]).zfill(2)
geom_hdf = ras.project_folder / f"{ras.project_name}.g{geometry_number}.hdf"
if not geom_hdf.exists():
raise FileNotFoundError(f"Geometry HDF not found: {geom_hdf}")
return geom_hdf
def mesh_precip_summary(plan_hdf: Path, variable: str) -> pd.DataFrame:
mesh_data = HdfResultsMesh.get_mesh_cells_timeseries(plan_hdf, var=variable)
records = []
for mesh_name, dataset in mesh_data.items():
values = dataset[variable].values
final_values = values[-1]
records.append(
{
"mesh": mesh_name,
"timesteps": int(values.shape[0]),
"cells": int(values.shape[1]),
"min": float(np.nanmin(values)),
"mean_final": float(np.nanmean(final_values)),
"max": float(np.nanmax(values)),
"positive_final_cells": int(np.count_nonzero(final_values > 0)),
}
)
return pd.DataFrame.from_records(records)
Extract the Example Project¶
Python
project_path = RasExamples.extract_project(
PROJECT_NAME,
output_path=RUN_ROOT,
suffix=PROJECT_SUFFIX,
)
ras = init_ras_project(
project_path,
ras_version=RAS_VERSION,
load_results_summary=False,
)
plan_row, starting_precip_config = find_gridded_precipitation_plan(ras)
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 = get_plan_geometry_hdf(ras, plan_row)
BOUNDS = tuple(
float(value)
for value in HdfProject.get_project_bounds_latlon(GEOMETRY_HDF, buffer_percent=10.0)
)
print(f"Project folder: {ras.project_folder}")
print(f"Selected plan: p{PLAN_NUMBER}; unsteady file: u{UNSTEADY_NUMBER}; geometry: g{GEOMETRY_NUMBER}")
print(f"Starting precipitation source: {starting_precip_config.get('source')}")
print(f"Project bounds with 10% buffer: {BOUNDS}")
ras.plan_df[["plan_number", "geometry_number", "unsteady_number"]]
Model Context¶
Python
mesh_areas = HdfMesh.get_mesh_areas(GEOMETRY_HDF)
mesh_summary = pd.DataFrame(
{
"project": [PROJECT_NAME],
"plan": [f"p{PLAN_NUMBER}"],
"geometry": [f"g{GEOMETRY_NUMBER}"],
"unsteady": [f"u{UNSTEADY_NUMBER}"],
"starting_precip_source": [starting_precip_config.get("source")],
"mesh_areas": [", ".join(mesh_areas["mesh_name"].astype(str).tolist())],
"simulation_start": [EVENT_START],
"simulation_end": [SIM_END],
}
)
display(mesh_summary)
fig, ax = plt.subplots(figsize=(7, 5))
plot_mesh = mesh_areas.to_crs("EPSG:4326") if mesh_areas.crs else mesh_areas
plot_mesh.boundary.plot(ax=ax, color="#1f4e79", linewidth=1.5, label="2D mesh boundary")
west, south, east, north = BOUNDS
ax.plot(
[west, east, east, west, west],
[south, south, north, north, south],
color="#b84a39",
linewidth=1.4,
linestyle="--",
label="MRMS download bounds",
)
ax.set_title("Bald Eagle RasExamples model and MRMS query extent")
ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude")
ax.grid(True, alpha=0.25)
ax.legend(loc="best")
plt.show()
Download and Inspect MRMS QPE¶
Python
catalog = PrecipMrms.catalog(
bounds=BOUNDS,
start_date=EVENT_START,
end_date=EVENT_LAST_QPE,
source="noaa_s3",
)
expected_files = int((EVENT_LAST_QPE - EVENT_START).total_seconds() // 3600) + 1
assert len(catalog) >= expected_files, f"Expected {expected_files} MRMS frames, found {len(catalog)}"
mrms_files = PrecipMrms.download(
bounds=BOUNDS,
start_time=EVENT_START,
end_time=EVENT_LAST_QPE,
output_dir=ras.project_folder / "P" / "mrms" / "grib2",
source="noaa_s3",
)
assert len(mrms_files) >= expected_files
mrms_stack = PrecipMrms.load_grib2_stack(mrms_files, bounds=BOUNDS)
hyetograph = PrecipMrms.to_hyetograph(mrms_stack)
assert float(hyetograph["cumulative_depth"].iloc[-1]) > 0.0
print(f"Downloaded {len(mrms_files)} MRMS GRIB2 files")
print(f"MRMS stack shape: {mrms_stack.shape}")
print(f"Spatial-average event depth: {hyetograph['cumulative_depth'].iloc[-1]:.3f} inches")
display(catalog[["valid_time", "archive_product", "filename", "compressed"]])
display(hyetograph)
Python
fig, ax1 = plt.subplots(figsize=(9, 4))
ax1.bar(
hyetograph["time"],
hyetograph["incremental_depth"],
width=0.03,
color="#2f6f9f",
label="Incremental depth",
)
ax1.set_ylabel("Incremental depth (in)")
ax1.set_xlabel("MRMS valid time")
ax1.grid(True, axis="y", alpha=0.25)
ax2 = ax1.twinx()
ax2.plot(
hyetograph["time"],
hyetograph["cumulative_depth"],
color="#b84a39",
marker="o",
linewidth=2,
label="Cumulative depth",
)
ax2.set_ylabel("Cumulative depth (in)")
handles1, labels1 = ax1.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(handles1 + handles2, labels1 + labels2, loc="upper left")
ax1.set_title("MRMS QPE over Bald Eagle example-project bounds")
fig.autofmt_xdate()
plt.show()
Export NetCDF and Configure HEC-RAS Gridded Precipitation¶
Python
mrms_netcdf = PrecipMrms.to_ras_netcdf(
mrms_stack,
ras.project_folder / "P" / "mrms" / "mrms_qpe.nc",
resolution=MRMS_RESOLUTION_METERS,
)
assert mrms_netcdf.exists() and mrms_netcdf.stat().st_size > 0
RasUnsteady.set_gridded_precipitation(
UNSTEADY_NUMBER,
mrms_netcdf,
interpolation="Bilinear",
ras_object=ras,
dataset_name="APCP_surface",
)
updated_precip_config = RasUnsteady.get_met_precipitation_config(
UNSTEADY_NUMBER,
ras_object=ras,
)
assert updated_precip_config["enabled"] is True
assert updated_precip_config["source"] == "GDAL Raster File(s)"
assert updated_precip_config["gdal_group"] == "APCP_surface"
print(f"NetCDF: {mrms_netcdf}")
print("Updated precipitation configuration:")
for key in ["enabled", "mode", "source", "gdal_filename", "gdal_group"]:
print(f" {key}: {updated_precip_config.get(key)}")
print(f"Unsteady HDF sidecar: {Path(str(ras.project_folder / f'{ras.project_name}.u{UNSTEADY_NUMBER}') + '.hdf')}")
Compute the Plan¶
Python
RasPlan.update_simulation_date(
PLAN_NUMBER,
EVENT_START,
SIM_END,
ras_object=ras,
)
RasPlan.update_plan_intervals(
PLAN_NUMBER,
output_interval="1HOUR",
instantaneous_interval="1HOUR",
mapping_interval="1HOUR",
ras_object=ras,
)
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 bool(compute_result), "HEC-RAS compute failed"
PLAN_HDF = ras.project_folder / f"{ras.project_name}.p{PLAN_NUMBER}.hdf"
assert PLAN_HDF.exists() and PLAN_HDF.stat().st_size > 0
print(f"Compute result: {compute_result}")
print(f"Plan HDF: {PLAN_HDF}")
Validate Model Output Precipitation¶
Python
cumulative_summary = mesh_precip_summary(
PLAN_HDF,
"Cell Cumulative Precipitation Depth",
)
rate_summary = mesh_precip_summary(
PLAN_HDF,
"Cell Precipitation Rate",
)
assert int(cumulative_summary["positive_final_cells"].sum()) > 0
assert float(cumulative_summary["max"].max()) > 0.0
assert float(rate_summary["max"].max()) > 0.0
display(cumulative_summary)
display(rate_summary)
Python
mesh_data = HdfResultsMesh.get_mesh_cells_timeseries(
PLAN_HDF,
var="Cell Cumulative Precipitation Depth",
)
mesh_name, dataset = next(iter(mesh_data.items()))
cell_values = dataset["Cell Cumulative Precipitation Depth"]
time_index = pd.to_datetime(dataset["time"].values)
mean_cumulative = cell_values.mean(dim="cell_id").values
max_cumulative = cell_values.max(dim="cell_id").values
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(time_index, mean_cumulative, marker="o", label="Mesh mean")
ax.plot(time_index, max_cumulative, marker="o", label="Mesh max")
ax.set_title(f"HEC-RAS output precipitation applied to {mesh_name}")
ax.set_xlabel("Simulation time")
ax.set_ylabel("Cumulative precipitation depth (in)")
ax.grid(True, alpha=0.25)
ax.legend()
fig.autofmt_xdate()
plt.show()
print(
f"Final mesh mean: {mean_cumulative[-1]:.3f} in; "
f"final mesh max: {max_cumulative[-1]:.3f} in"
)
Validate Hydraulic Response¶
Python
depth_summary = mesh_precip_summary(
PLAN_HDF,
"Cell Hydraulic Depth",
).rename(
columns={
"mean_final": "mean_final_depth",
"max": "max_depth",
"positive_final_cells": "wet_final_cells",
}
)
assert float(depth_summary["max_depth"].max()) > 0.0
assert int(depth_summary["wet_final_cells"].sum()) > 0
display(depth_summary)
depth_data = HdfResultsMesh.get_mesh_cells_timeseries(
PLAN_HDF,
var="Cell Hydraulic Depth",
)
mesh_name, depth_dataset = next(iter(depth_data.items()))
depth_values = depth_dataset["Cell Hydraulic Depth"]
depth_time = pd.to_datetime(depth_dataset["time"].values)
mean_depth = depth_values.mean(dim="cell_id").values
max_depth = depth_values.max(dim="cell_id").values
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(depth_time, mean_depth, marker="o", label="Mesh mean depth")
ax.plot(depth_time, max_depth, marker="o", label="Mesh max depth")
ax.set_title(f"HEC-RAS hydraulic depth response in {mesh_name}")
ax.set_xlabel("Simulation time")
ax.set_ylabel("Hydraulic depth (ft)")
ax.grid(True, alpha=0.25)
ax.legend()
fig.autofmt_xdate()
plt.show()
print(
f"Final mesh mean depth: {mean_depth[-1]:.3f} ft; "
f"peak mesh max depth: {max_depth.max():.3f} ft"
)