2D Connection Culvert Invert Validation (Terrain Cell Minimum)¶
Development Mode¶
Set USE_LOCAL_SOURCE = True when running from a local ras-commander checkout.
USE_LOCAL_SOURCE = False
if USE_LOCAL_SOURCE:
import sys
from pathlib import Path
cwd = Path.cwd()
local_path = cwd if (cwd / "ras_commander").exists() else cwd.parent
if str(local_path) not in sys.path:
sys.path.insert(0, str(local_path))
print(f"LOCAL SOURCE MODE: {local_path / 'ras_commander'}")
else:
print("PIP PACKAGE MODE")
import logging
import os
import warnings
from pathlib import Path
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from IPython.display import display
from shapely.geometry import LineString, Point
from ras_commander import RasExamples
from ras_commander.geom import GeomLateral, GeomCulvertGIS
from ras_commander.hdf import HdfMesh
warnings.filterwarnings("ignore", category=FutureWarning)
logging.getLogger("ras_commander").setLevel(logging.CRITICAL)
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 200)
import ras_commander
print(f"Loaded: {ras_commander.__file__}")
PIP PACKAGE MODE
c:\Users\bill\anaconda3\envs\rascommander\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
Loaded: G:\GH\ras-commander-wt-culv2d\ras_commander\__init__.py
Validating culvert inverts on a 2D connection¶
In a 2D model, a culvert through an embankment is placed on a SA/2D connection (the connection centerline is a real GIS cut line in the geometry). Before adding or moving such a culvert, its invert must not sit below the streambed -- and in 2D the relevant streambed is the minimum terrain elevation of the mesh cell the culvert end discharges into.
GeomCulvertGIS computes that cell minimum directly from the terrain raster
(mesh_cell_min_from_terrain), so the check works without running the HEC-RAS
2D geometry preprocessor and reflects any terrain modifications baked into the
raster. validate_2d_inverts then flags proposed inverts that fall below their
cell minimum.
This notebook runs entirely on mesh geometry + terrain -- no HEC-RAS compute.
Load the model and pick a connection¶
We use BaldEagleCrkMulti2D and its 2D-to-2D "Lower Levee" connection -- an
embankment inside the mesh, with cells on both sides (where culverts would
discharge).
WORK = Path(os.environ.get("RAS_COMMANDER_WORKDIR",
(Path.cwd() if (Path.cwd() / "ras_commander").exists() else Path.cwd().parent)
/ "working" / "culvert_gis_2d"))
WORK.mkdir(parents=True, exist_ok=True)
project_path = Path(RasExamples.extract_project("BaldEagleCrkMulti2D", output_path=WORK,
suffix="culv2d"))
GEOM = project_path / "BaldEagleDamBrk.g01"
GEOM_HDF = project_path / "BaldEagleDamBrk.g01.hdf"
conns = GeomLateral.get_connections(GEOM)
display(conns[["Name", "Type", "From", "To", "NumPoints", "HasGate", "HasCulvert"]])
CONN = "Lower Levee"
line = GeomLateral.get_connection_line_coords(GEOM, CONN)
conn_ls = LineString(line[["X", "Y"]].to_numpy())
print(f"Connection '{CONN}': {len(line)} vertices, {conn_ls.length:,.0f} ft")
| Name | Type | From | To | NumPoints | HasGate | HasCulvert | |
|---|---|---|---|---|---|---|---|
| 0 | Dam | SA to 2D | Reservoir Pool | BaldEagleCr | 10 | True | False |
| 1 | Lower Levee | 2D to 2D | BaldEagleCr | BaldEagleCr | 94 | False | False |
| 2 | Middle Levee | 2D to 2D | BaldEagleCr | BaldEagleCr | 122 | False | False |
| 3 | Upper Levee | 2D to 2D | BaldEagleCr | BaldEagleCr | 99 | False | False |
Connection 'Lower Levee': 89 vertices, 15,808 ft
The connection cut line over the 2D mesh¶
mesh_crs = HdfMesh.get_mesh_cell_polygons(GEOM_HDF).crs
cells = HdfMesh.get_mesh_cell_polygons(GEOM_HDF)
# clip cells to a buffer around the connection for a readable plan view
buf = conn_ls.buffer(800)
near = cells[cells.intersects(buf)]
print(f"{len(near):,} mesh cells within 800 ft of the connection (of {len(cells):,})")
fig, ax = plt.subplots(figsize=(9, 7))
near.boundary.plot(ax=ax, color="0.8", linewidth=0.4)
gpd.GeoSeries([conn_ls], crs=mesh_crs).plot(ax=ax, color="C3", linewidth=2.5)
ax.set_title(f"BaldEagle 2D - '{CONN}' connection over the mesh")
ax.set_xlabel("Easting (ft)"); ax.set_ylabel("Northing (ft)")
ax.set_aspect("equal", adjustable="box"); ax.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()
3,329 mesh cells within 800 ft of the connection (of 87,039)

Terrain minimum of the cell at each culvert location¶
Sample candidate culvert locations along the connection and compute the terrain minimum of the mesh cell each one sits in -- the elevation a culvert invert must stay at or above. This is the envelope a valid invert cannot drop below.
n_sites = 7
sites = [(p.x, p.y) for p in
(conn_ls.interpolate(d, normalized=True) for d in np.linspace(0.1, 0.9, n_sites))]
cmin = GeomCulvertGIS.mesh_cell_min_from_terrain(GEOM_HDF, sites)
cmin["dist_ft"] = [conn_ls.project(Point(x, y)) for x, y in sites]
display(cmin[["point", "cell_id", "dist_ft", "cell_terrain_min"]].round(2))
fig, ax = plt.subplots(figsize=(9, 4.5))
ax.plot(cmin["dist_ft"], cmin["cell_terrain_min"], marker="o", color="C0",
label="cell terrain minimum (invert must stay above)")
ax.fill_between(cmin["dist_ft"], cmin["cell_terrain_min"],
cmin["cell_terrain_min"].min() - 5, color="0.9")
ax.set_xlabel("Distance along connection (ft)")
ax.set_ylabel("Elevation (ft)")
ax.set_title(f"Terrain cell-minimum along '{CONN}' (no preprocessing)")
ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()
| point | cell_id | dist_ft | cell_terrain_min | |
|---|---|---|---|---|
| 0 | 0 | 86034 | 1580.79 | 558.69 |
| 1 | 1 | 86120 | 3688.51 | 537.62 |
| 2 | 2 | 86204 | 5796.22 | 538.06 |
| 3 | 3 | 86288 | 7903.94 | 538.47 |
| 4 | 4 | 86372 | 10011.66 | 540.47 |
| 5 | 5 | 86459 | 12119.38 | 539.12 |
| 6 | 6 | 86542 | 14227.10 | 547.28 |

Validate a proposed culvert: pre (too low) -> post (corrected)¶
A retrofit proposes culverts at these sites. Pre: all set to a single low invert (a common drafting placeholder) -- several fall below their cell terrain minimum. Post: each invert is raised to clear its local cell minimum.
floor = float(cmin["cell_terrain_min"].min())
pre_inverts = [floor - 1.0] * n_sites # one low placeholder
post_inverts = [m + 0.5 for m in cmin["cell_terrain_min"]] # 0.5 ft above local cell min
pre = GeomCulvertGIS.validate_2d_inverts(GEOM_HDF, sites, pre_inverts)
post = GeomCulvertGIS.validate_2d_inverts(GEOM_HDF, sites, post_inverts)
print("PRE (single low invert):", pre["status"].value_counts().to_dict())
print("POST (raised to local cell min):", post["status"].value_counts().to_dict())
display(pre[["point", "cell_id", "cell_terrain_min", "invert", "status", "detail"]])
PRE (single low invert): {'FAIL': 7}
POST (raised to local cell min): {'PASS': 7}
| point | cell_id | cell_terrain_min | invert | status | detail | |
|---|---|---|---|---|---|---|
| 0 | 0 | 86034 | 558.68750 | 536.625 | FAIL | invert 536.62 is 22.06 ft below cell terrain m... |
| 1 | 1 | 86120 | 537.62500 | 536.625 | FAIL | invert 536.62 is 1.00 ft below cell terrain mi... |
| 2 | 2 | 86204 | 538.06250 | 536.625 | FAIL | invert 536.62 is 1.44 ft below cell terrain mi... |
| 3 | 3 | 86288 | 538.46875 | 536.625 | FAIL | invert 536.62 is 1.84 ft below cell terrain mi... |
| 4 | 4 | 86372 | 540.46875 | 536.625 | FAIL | invert 536.62 is 3.84 ft below cell terrain mi... |
| 5 | 5 | 86459 | 539.12500 | 536.625 | FAIL | invert 536.62 is 2.50 ft below cell terrain mi... |
| 6 | 6 | 86542 | 547.28125 | 536.625 | FAIL | invert 536.62 is 10.66 ft below cell terrain m... |
Plan view: failing vs corrected placements¶
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
for ax, rep, title in [(axes[0], pre, "PRE: single low invert"),
(axes[1], post, "POST: raised to local cell minimum")]:
near.boundary.plot(ax=ax, color="0.85", linewidth=0.3)
gpd.GeoSeries([conn_ls], crs=mesh_crs).plot(ax=ax, color="0.4", linewidth=1.5)
ok = rep[rep["status"] == "PASS"]
bad = rep[rep["status"] == "FAIL"]
ax.scatter(ok["x"], ok["y"], c="C2", s=70, zorder=5, label="PASS", edgecolor="k")
ax.scatter(bad["x"], bad["y"], c="C3", s=90, marker="X", zorder=5, label="FAIL", edgecolor="k")
ax.set_title(title); ax.set_aspect("equal", adjustable="box")
ax.legend(); ax.grid(True, alpha=0.3)
ax.set_xlabel("Easting (ft)"); ax.set_ylabel("Northing (ft)")
plt.tight_layout(); plt.show()

An actual authored connection culvert (real Connection Culv= format)¶
The check above validates proposed invert points. To show the end-to-end case we also authored a real 2D connection culvert onto the Lower Levee connection and confirmed HEC-RAS computes it.
Provenance (how the culvert below was created). Production 2D models -- e.g. the
Louisiana Watershed Initiative LWI HEC-RAS models -- place culverts on a SA/2D
connection with a Connection Culv= record whose barrel stores explicit GIS
endpoint coordinates (a Conn Culvert Barrel= line followed by a packed
US_x US_y DS_x DS_y coordinate line). Using that format we reasoned a retrofit
culvert for the BaldEagle Lower Levee embankment:
- located it at the lowest terrain point along the connection (where flow concentrates),
- ran the barrel perpendicular to the connection (through the embankment), length 60 ft, so the two GIS endpoints sit 60 ft apart,
- set the US/DS inverts just above each end-cell's terrain minimum (using the same terrain check as above), with a mild slope,
- wrote the
Connection Culv=block into the geometry and verified it computes in HEC-RAS (a 2D unsteady run completed:SUCCESS, no geometry/data errors).
The fixture-generation code is intentionally omitted -- it is one-off test-fixture authoring. Below we simply inline the resulting record/coordinates and validate them. This is the exact block HEC-RAS accepted:
# --- authored retrofit culvert (inlined fixture; generation code omitted) ----
# The verbatim Connection Culv= block written into BaldEagleDamBrk.g01 and accepted
# by HEC-RAS (2D unsteady run completed, no data_errors):
connection_culv_block = """Connection Culv=2,8.0,6.0,60.0,0.024,0.5,1,8,1,539.56,539.06, 1 ,Retrofit Culv, 0 ,
7113.55 7113.55
Conn Culvert Barrel=1,8x6 RCB,2
2055956.24304882353790.6303050362055952.00148554353730.780416641
Conn Culv Bottom n=0.024
Conn HTab FreeFlow Pts= 100
Conn HTab Sub Flow Curves= 60
Conn HTab Sub Flow Pts= 50 """
print(connection_culv_block)
# decoded record + the per-barrel GIS endpoint coordinates (mesh CRS, EPSG:2271)
authored = {
"connection": "Lower Levee", "shape": "Box (code 2)",
"span_ft": 8.0, "rise_ft": 6.0, "length_ft": 60.0,
"manning_n": 0.024, "Ke": 0.5, "Kex": 1.0,
"us_xy": (2055956.24304882, 353790.63030504), # upstream barrel end
"ds_xy": (2055952.00148554, 353730.78041664), # downstream barrel end
"us_invert": 539.56, "ds_invert": 539.06,
}
Connection Culv=2,8.0,6.0,60.0,0.024,0.5,1,8,1,539.56,539.06, 1 ,Retrofit Culv, 0 ,
7113.55 7113.55
Conn Culvert Barrel=1,8x6 RCB,2
2055956.24304882353790.6303050362055952.00148554353730.780416641
Conn Culv Bottom n=0.024
Conn HTab FreeFlow Pts= 100
Conn HTab Sub Flow Curves= 60
Conn HTab Sub Flow Pts= 50
Validate the authored culvert¶
Because a 2D connection culvert stores its barrel endpoints, the +/-1% GIS-length
rule is exact here (no reconstruction needed): the endpoint-to-endpoint distance is
compared directly to the entered Length. The inverts are checked against the
terrain minimum of each end's mesh cell, exactly as above.
import math
# 1) GIS cut-line length vs entered Length -- EXACT (endpoints are stored)
gis_len = math.dist(authored["us_xy"], authored["ds_xy"])
length_err = abs(gis_len - authored["length_ft"]) / authored["length_ft"] * 100
print(f"barrel GIS length {gis_len:.2f} ft vs entered {authored['length_ft']:.1f} ft "
f"-> {length_err:.2f}% ({'PASS' if length_err <= 1.0 else 'FAIL'} the HEC-RAS +/-1% rule)")
# 2) US/DS inverts vs the terrain minimum of each barrel-end mesh cell
inv_rep = GeomCulvertGIS.validate_2d_inverts(
GEOM_HDF, [authored["us_xy"], authored["ds_xy"]],
[authored["us_invert"], authored["ds_invert"]])
display(inv_rep[["point", "cell_id", "cell_terrain_min", "invert", "status", "detail"]])
barrel GIS length 60.00 ft vs entered 60.0 ft -> 0.00% (PASS the HEC-RAS +/-1% rule)
| point | cell_id | cell_terrain_min | invert | status | detail | |
|---|---|---|---|---|---|---|
| 0 | 0 | 86257 | 536.09375 | 539.56 | PASS | invert 539.56 >= cell terrain min 536.09 |
| 1 | 1 | 86256 | 539.06250 | 539.06 | PASS | invert 539.06 >= cell terrain min 539.06 |
Plan view: the authored barrel on the connection¶
fig, ax = plt.subplots(figsize=(9, 7))
near.boundary.plot(ax=ax, color="0.85", linewidth=0.3)
gpd.GeoSeries([conn_ls], crs=mesh_crs).plot(ax=ax, color="0.4", linewidth=1.5,
label="connection")
bx = [authored["us_xy"][0], authored["ds_xy"][0]]
by = [authored["us_xy"][1], authored["ds_xy"][1]]
ax.plot(bx, by, color="C3", linewidth=3, label="authored culvert barrel")
ax.scatter(bx, by, color="C3", s=60, zorder=5, edgecolor="k")
# zoom to the culvert
ax.set_xlim(min(bx) - 400, max(bx) + 400)
ax.set_ylim(min(by) - 400, max(by) + 400)
ax.set_title("Authored retrofit culvert on the Lower Levee connection")
ax.set_aspect("equal", adjustable="box"); ax.legend(); ax.grid(True, alpha=0.3)
ax.set_xlabel("Easting (ft)"); ax.set_ylabel("Northing (ft)")
plt.tight_layout(); plt.show()

Summary¶
summary = pd.DataFrame([
{"Design": "PRE (single low invert)", "invert basis": f"{floor - 1.0:.1f} ft (flat)",
"PASS": int((pre["status"] == "PASS").sum()), "FAIL": int((pre["status"] == "FAIL").sum())},
{"Design": "POST (raised to local cell min)", "invert basis": "local cell min + 0.5 ft",
"PASS": int((post["status"] == "PASS").sum()), "FAIL": int((post["status"] == "FAIL").sum())},
])
display(summary)
print("The terrain cell-minimum check (no HEC-RAS preprocessing) flags culvert "
"inverts set below the streambed of their 2D discharge cell, and confirms "
"the corrected placement. 2D connection invert-validation workflow complete.")
| Design | invert basis | PASS | FAIL | |
|---|---|---|---|---|
| 0 | PRE (single low invert) | 536.6 ft (flat) | 0 | 7 |
| 1 | POST (raised to local cell min) | local cell min + 0.5 ft | 7 | 0 |
The terrain cell-minimum check (no HEC-RAS preprocessing) flags culvert inverts set below the streambed of their 2D discharge cell, and confirms the corrected placement. 2D connection invert-validation workflow complete.