Skip to content

2D Connection Culvert Invert Validation (Terrain Cell Minimum)

Python
#!pip install --upgrade ras-commander

Development Mode

Set USE_LOCAL_SOURCE = True when running from a local ras-commander checkout.

Python
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__}")
Text Only
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).

Python
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
Text Only
Connection 'Lower Levee': 89 vertices, 15,808 ft

The connection cut line over the 2D mesh

Python
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()
Text Only
3,329 mesh cells within 800 ft of the connection (of 87,039)

png

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.

Python
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

png

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.

Python
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"]])
Text Only
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

Python
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()

png

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:

Python
# --- 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,
}
Text Only
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.

Python
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"]])
Text Only
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

Python
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()

png

Summary

Python
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
Text Only
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.
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.