Pipe Network Mesh Generation¶
Overview¶
This notebook demonstrates headless mesh generation on a project with pipe networks. The Davis example project (HEC-RAS Pipes beta) contains:
- 2 flow areas:
area2(main, 150ft cell size) andDS Channel(small, 200ft) - 133 pipe nodes with 4 top inlets and 1 side inlet
- 132 pipe conduits connecting to the 2D mesh
- 1 pump station
A Note on Cell Counts (Seeds vs Computational Cells)¶
HEC-RAS stores two related-but-different numbers for a 2D flow area:
- Seed points — the
Storage Area 2D Points=count in the plain-text.g##file (and RasMapper'sNonVirtualCellCount). Forarea2this is 2,705. - Computational cells — the mesh the HEC-RAS geometry preprocessor builds from those
seeds and stores in the
.g##.hdf. Forarea2this is 2,941.
GeomMesh.generate() returns the seed count (2,705). The HDF computational-cell count
(2,941) is the authoritative mesh, so this notebook reports the HDF count everywhere
for consistency with the geometry HEC provided.
What This Notebook Proves¶
GeomMesh.generate()works on geometries with pipe network connections.NET geom.Save()automatically recomputes pipe network face/cell/node tables- Multi-area geometries are handled correctly (only the target area is modified)
- Regeneration reproduces the identical HEC-RAS computational mesh — same 2,941 cells, same positions to machine precision
- The
HdfPipeAPI reads the regenerated pipe network (nodes, conduits, inlets) correctly
Setup¶
USE_LOCAL_SOURCE = True
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:
from pathlib import Path
print("PIP PACKAGE MODE: Loading installed ras-commander")
from ras_commander import init_ras_project, RasExamples, RasPlan
from ras_commander.geom.GeomMesh import GeomMesh
from ras_commander.hdf.HdfMesh import HdfMesh
import h5py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib.patches import Patch
from matplotlib.lines import Line2D
from pathlib import Path
import ras_commander
print(f"Loaded: {ras_commander.__file__}")
LOCAL SOURCE MODE: Loading from G:\GH\ras-commander/ras_commander
Loaded: G:\GH\ras-commander\ras_commander\__init__.py
Extract Project and Initialize¶
PROJECT_NAME = "Davis"
RAS_VERSION = "6.6"
SUFFIX = "231_pipes"
project_folder = RasExamples.extract_project(PROJECT_NAME, suffix=SUFFIX)
ras = init_ras_project(project_folder, RAS_VERSION)
print(f"Project: {project_folder}")
print(f"\nPlans:")
print(ras.plan_df[['plan_number', 'Plan Title']].to_string(index=False))
print(f"\nGeometries:")
print(ras.geom_df[['geom_number', 'geom_title']].to_string(index=False))
2026-06-02 13:23:09 - ras_commander.RasExamples - INFO - Successfully extracted project 'Davis' to G:\GH\ras-commander\examples\example_projects\Davis_231_pipes
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 7.0 at C:\Program Files (x86)\HEC\HEC-RAS\7.0\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.7 Beta 5 at C:\Program Files (x86)\HEC\HEC-RAS\6.7 Beta 5\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.5 at C:\Program Files (x86)\HEC\HEC-RAS\6.5\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.3.1 at C:\Program Files (x86)\HEC\HEC-RAS\6.3.1\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.2 at C:\Program Files (x86)\HEC\HEC-RAS\6.2\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.1 at C:\Program Files (x86)\HEC\HEC-RAS\6.1\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.0 at C:\Program Files (x86)\HEC\HEC-RAS\6.0\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0.7 at C:\Program Files (x86)\HEC\HEC-RAS\5.0.7\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0.6 at C:\Program Files (x86)\HEC\HEC-RAS\5.0.6\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0.5 at C:\Program Files (x86)\HEC\HEC-RAS\5.0.5\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0.4 at C:\Program Files (x86)\HEC\HEC-RAS\5.0.4\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0.3 at C:\Program Files (x86)\HEC\HEC-RAS\5.0.3\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0.1 at C:\Program Files (x86)\HEC\HEC-RAS\5.0.1\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 5.0 at C:\Program Files (x86)\HEC\HEC-RAS\5.0\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 4.1.0 at C:\Program Files (x86)\HEC\HEC-RAS\4.1.0\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 4.0 at C:\Program Files (x86)\HEC\HEC-RAS\4.0\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered HEC-RAS 6.6 at C:\Program Files (x86)\HEC\HEC-RAS\6.6\Ras.exe via filesystem (x86)
2026-06-02 13:23:09 - ras_commander.RasUtils - INFO - Discovered 17 installed HEC-RAS version(s)
2026-06-02 13:23:09 - ras_commander.RasPrj - INFO - HEC-RAS 6.6 found via version discovery: C:\Program Files (x86)\HEC\HEC-RAS\6.6\Ras.exe
2026-06-02 13:23:10 - ras_commander.RasMap - INFO - Successfully parsed RASMapper file: G:\GH\ras-commander\examples\example_projects\Davis_231_pipes\DavisStormSystem.rasmap
2026-06-02 13:23:10 - ras_commander.RasPrj - INFO - ras-commander v0.97.0 | An open-source project of CLB Engineering Corporation (https://clbengineering.com/) | Docs: https://ras-commander.readthedocs.io | GitHub: https://github.com/gpt-cmdr/ras-commander
2026-06-02 13:23:10 - ras_commander.RasPrj - INFO - Project initialized: DavisStormSystem | Folder: G:\GH\ras-commander\examples\example_projects\Davis_231_pipes
2026-06-02 13:23:10 - ras_commander.RasPrj - INFO - Using HEC-RAS executable: C:\Program Files (x86)\HEC\HEC-RAS\6.6\Ras.exe
2026-06-02 13:23:10 - ras_commander.RasPrj - INFO -
═══════════════════════════════════════════════════════════════════════
ras-commander | HEC-RAS Automation Library
Docs: https://gpt-cmdr.github.io/ras-commander/
Repo: https://github.com/gpt-cmdr/ras-commander
═══════════════════════════════════════════════════════════════════════
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).
═══════════════════════════════════════════════════════════════════════
Project: G:\GH\ras-commander\examples\example_projects\Davis_231_pipes
Plans:
plan_number Plan Title
02 Full System ROM with Pump
Geometries:
geom_number geom_title
02 Davis Full System w/ Pump
Inspect Existing Geometry¶
Read the pipe network topology and mesh state from the HDF before regeneration.
geom_path = Path(
ras.geom_df.loc[ras.geom_df['geom_number'] == '02', 'full_path'].values[0]
)
hdf_path = Path(str(geom_path) + '.hdf')
# Read text file state
text = geom_path.read_text(encoding='utf-8', errors='replace')
print("=== Flow Areas (from text) ===")
for line in text.splitlines():
if line.startswith('Storage Area='):
name = line.split('=', 1)[1].split(',')[0].strip()
print(f" {name}")
elif line.startswith('Storage Area Point Generation Data='):
print(f" Cell size: {line.split('=', 1)[1].strip()}")
elif line.startswith('Storage Area 2D Points='):
print(f" Seed points: {line.split('=', 1)[1].strip()}")
# Read HDF state — store for later comparison
print("\n=== Pipe Network (from HDF) ===")
with h5py.File(str(hdf_path), 'r') as f:
attrs = f['Geometry/2D Flow Areas/Attributes'][:]
for row in attrs:
name = row['Name'].decode().strip()
dx = float(row['Spacing dx'])
print(f" Flow area: {name} (cell size: {dx})")
cc = f.get(f'Geometry/2D Flow Areas/{name}/Cells Center Coordinate')
if cc is not None:
print(f" HDF cell centers: {cc.shape[0]}")
# Store original cell centers and pipe nodes for figures
orig_cc = f['Geometry/2D Flow Areas/area2/Cells Center Coordinate'][:]
pipe_node_pts = f['Geometry/Pipe Nodes/Points'][:]
conduit_polylines = f['Geometry/Pipe Conduits/Polyline Points'][:]
conduit_info = f['Geometry/Pipe Conduits/Polyline Info'][:]
nodes = f.get('Geometry/Pipe Nodes/Attributes')
conds = f.get('Geometry/Pipe Conduits/Attributes')
inlets_top = f.get('Geometry/Pipe Nodes/Top Inlets/Attributes')
inlets_side = f.get('Geometry/Pipe Nodes/Side Inlets/Attributes')
n_nodes = nodes.shape[0] if nodes is not None else 0
n_conds = conds.shape[0] if conds is not None else 0
n_top = inlets_top.shape[0] if inlets_top is not None else 0
n_side = inlets_side.shape[0] if inlets_side is not None else 0
print(f"\n Pipe nodes: {n_nodes}")
print(f" Pipe conduits: {n_conds}")
print(f" Top inlets: {n_top}")
print(f" Side inlets: {n_side}")
# Store original pipe network table shapes
pn = f['Geometry/Pipe Networks/Davis']
orig_face_shape = pn['Face Property Table'].shape
orig_cell_shape = pn['Cell Property Table'].shape
orig_node_shape = pn['Node Surface Connectivity'].shape
print(f"\n Pipe network tables (pre-regeneration):")
print(f" Face table: {orig_face_shape}")
print(f" Cell table: {orig_cell_shape}")
print(f" Node connectivity: {orig_node_shape}")
=== Flow Areas (from text) ===
area2
Cell size: 0,0,150,150
Seed points: 2705
DS Channel
Cell size: ,,200,200
Seed points: 11
=== Pipe Network (from HDF) ===
Flow area: area2 (cell size: 150.0)
HDF cell centers: 2941
Flow area: DS Channel (cell size: 200.0)
HDF cell centers: 36
Pipe nodes: 133
Pipe conduits: 132
Top inlets: 4
Side inlets: 1
Pipe network tables (pre-regeneration):
Face table: (1991,)
Cell table: (1992,)
Node connectivity: (133,)
Figure 1: Original Mesh with Pipe Network¶
Show the original mesh cell polygons, the 2D flow area perimeter, pipe node locations, and conduit lines.
# Build conduit line segments for plotting
def build_conduit_segments(polyline_pts, poly_info):
segments = []
for row in poly_info:
start = int(row[0])
count = int(row[1])
if count >= 2:
segments.append(polyline_pts[start:start + count])
return segments
conduit_segs = build_conduit_segments(conduit_polylines, conduit_info)
# Read mesh cell polygons and the 2D flow area perimeter from the original HDF
orig_polys = HdfMesh.get_mesh_cell_polygons(hdf_path)
orig_area2 = orig_polys[orig_polys['mesh_name'] == 'area2']
mesh_perims = HdfMesh.get_mesh_areas(hdf_path)
area2_perim = mesh_perims[mesh_perims['mesh_name'] == 'area2']
fig, ax = plt.subplots(1, 1, figsize=(12, 10))
# Mesh cell polygons
orig_area2.plot(ax=ax, facecolor='lightsteelblue', edgecolor='steelblue',
linewidth=0.25, alpha=0.6, zorder=1)
# 2D flow area perimeter
area2_perim.boundary.plot(ax=ax, color='black', linewidth=1.5, zorder=2)
# Pipe conduit lines
lc = LineCollection(conduit_segs, colors='gray', linewidths=0.6, alpha=0.7, zorder=3)
ax.add_collection(lc)
# Pipe nodes
ax.scatter(pipe_node_pts[:, 0], pipe_node_pts[:, 1], s=10, c='red', marker='o',
alpha=0.8, zorder=4)
# Manual legend (GeoDataFrame.plot does not populate handles for filled polygons)
legend_handles = [
Patch(facecolor='lightsteelblue', edgecolor='steelblue',
label=f'Mesh cells ({len(orig_cc):,})'),
Line2D([0], [0], color='black', lw=1.5, label='2D flow area perimeter'),
Line2D([0], [0], color='gray', lw=1.0, label=f'Pipe conduits ({len(conduit_segs)})'),
Line2D([0], [0], marker='o', color='w', markerfacecolor='red', markersize=7,
label=f'Pipe nodes ({len(pipe_node_pts)})'),
]
ax.legend(handles=legend_handles, loc='upper left')
ax.set_xlabel('Easting (ft)')
ax.set_ylabel('Northing (ft)')
ax.set_title('Davis Storm System: Original Mesh Cells + Pipe Network')
ax.set_aspect('equal')
ax.grid(alpha=0.2)
plt.tight_layout()
plt.savefig(Path(project_folder) / 'fig1_original_mesh_pipes.png', dpi=150, bbox_inches='tight')
plt.show()

Clone Geometry and Regenerate Mesh¶
Clone the geometry so the original is preserved, then regenerate the mesh for area2.
# Clone geometry
new_geom = RasPlan.clone_geom('02', ras_object=ras)
new_geom_path = Path(
ras.geom_df.loc[ras.geom_df['geom_number'] == new_geom, 'full_path'].values[0]
)
print(f"Cloned: g02 -> g{new_geom} ({new_geom_path.name})")
# Read pre-generation state
pre_text = new_geom_path.read_text(encoding='utf-8', errors='replace')
pre_counts = {}
current_area = None
for line in pre_text.splitlines():
if line.startswith('Storage Area='):
current_area = line.split('=', 1)[1].split(',')[0].strip()
elif line.startswith('Storage Area 2D Points=') and current_area:
pre_counts[current_area] = int(line.split('=', 1)[1].strip())
print(f"Pre-generation seed counts: {pre_counts}")
2026-06-02 13:23:11 - ras_commander.RasUtils - INFO - File cloned from G:\GH\ras-commander\examples\example_projects\Davis_231_pipes\DavisStormSystem.g02 to G:\GH\ras-commander\examples\example_projects\Davis_231_pipes\DavisStormSystem.g01
2026-06-02 13:23:11 - ras_commander.RasUtils - INFO - File cloned from G:\GH\ras-commander\examples\example_projects\Davis_231_pipes\DavisStormSystem.g02.hdf to G:\GH\ras-commander\examples\example_projects\Davis_231_pipes\DavisStormSystem.g01.hdf
2026-06-02 13:23:11 - ras_commander.RasUtils - INFO - Project file updated with new Geom entry: 01
2026-06-02 13:23:11 - ras_commander.RasMap - INFO - Successfully parsed RASMapper file: G:\GH\ras-commander\examples\example_projects\Davis_231_pipes\DavisStormSystem.rasmap
Cloned: g02 -> g01 (DavisStormSystem.g01)
Pre-generation seed counts: {'area2': 2705, 'DS Channel': 11}
# Generate mesh for area2 (the main flow area with pipe connections)
print("Generating mesh for area2 (cell_size=150)...")
result = GeomMesh.generate(
geom_number=new_geom_path,
mesh_name='area2',
cell_size=150.0,
max_iterations=10,
ras_object=ras,
)
print(f"\nResult:")
print(f" Status: {result.status}")
print(f" Faces: {result.face_count:,}")
print(f" Iterations: {result.iterations}")
if result.fixes_applied:
print(f" Fixes: {result.fixes_applied}")
if result.error_message:
print(f" Error: {result.error_message}")
# Reconcile the two cell counts (see the "Note on Cell Counts" in the Overview):
# result.cell_count is RasMapper's NonVirtualCellCount == the number of 2D seed points
# written to "Storage Area 2D Points=" in the .g## text. The authoritative HEC-RAS
# computational mesh is read from the .g##.hdf and is what we report from here on.
new_hdf_path = Path(str(new_geom_path) + '.hdf')
with h5py.File(str(new_hdf_path), 'r') as f:
hdf_cell_count = f['Geometry/2D Flow Areas/area2/Cells Center Coordinate'].shape[0]
print(f"\n Seed points (RasMapper / .g## text): {result.cell_count:,}")
print(f" Computational cells (.g##.hdf): {hdf_cell_count:,} <- used for all figures/counts below")
Generating mesh for area2 (cell_size=150)...
2026-06-02 13:23:13 - ras_commander.geom.GeomMesh - INFO - [area2] 57-point perimeter from .NET
2026-06-02 13:23:13 - ras_commander.geom.GeomMesh - INFO - Seeds via .NET RegenerateMeshPoints: 2705 (0 breaklines incl 0 struct, 2 perimeters)
2026-06-02 13:23:13 - ras_commander.geom.GeomMesh - INFO - [area2] 2705 seeds via .NET RegenerateMeshPoints
2026-06-02 13:23:14 - ras_commander.geom.GeomMesh - INFO - Text seeds patched → 2705 points in DavisStormSystem.g01
2026-06-02 13:23:14 - ras_commander.geom.GeomMesh - INFO - [area2] Mesh complete: 2705 cells, 5555 faces in 1 iteration(s)
Result:
Status: complete
Faces: 5,555
Iterations: 1
Seed points (RasMapper / .g## text): 2,705
Computational cells (.g##.hdf): 2,941 <- used for all figures/counts below
Figure 2: Before vs After — Mesh Cell Comparison¶
Side-by-side comparison of the original and regenerated mesh cell polygons with the 2D flow area perimeter and pipe nodes. Both meshes contain the same 2,941 HEC-RAS computational cells (the regenerated seeds reproduce the original mesh exactly).
# Read regenerated mesh cell polygons and perimeter
new_hdf_path = Path(str(new_geom_path) + '.hdf')
regen_polys = HdfMesh.get_mesh_cell_polygons(new_hdf_path)
regen_area2 = regen_polys[regen_polys['mesh_name'] == 'area2']
regen_perims = HdfMesh.get_mesh_areas(new_hdf_path)
regen_area2_perim = regen_perims[regen_perims['mesh_name'] == 'area2']
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 9))
for ax, polys, perim, title, fc, ec in [
(ax1, orig_area2, area2_perim,
f'Original ({len(orig_cc):,} cells in HDF)', 'lightsteelblue', 'steelblue'),
(ax2, regen_area2, regen_area2_perim,
f'Regenerated ({hdf_cell_count:,} cells in HDF)', 'navajowhite', 'darkorange'),
]:
polys.plot(ax=ax, facecolor=fc, edgecolor=ec, linewidth=0.25, alpha=0.6, zorder=1)
perim.boundary.plot(ax=ax, color='black', linewidth=1.3, zorder=2)
lc = LineCollection(conduit_segs, colors='gray', linewidths=0.5, alpha=0.6, zorder=3)
ax.add_collection(lc)
ax.scatter(pipe_node_pts[:, 0], pipe_node_pts[:, 1], s=10, c='red',
marker='o', alpha=0.8, zorder=4)
ax.set_title(title, fontsize=12)
ax.set_xlabel('Easting (ft)')
ax.set_ylabel('Northing (ft)')
ax.set_aspect('equal')
ax.grid(alpha=0.2)
# Match axis limits using the mesh bounding box
minx, miny, maxx, maxy = orig_area2.total_bounds
pad = 200
for ax in (ax1, ax2):
ax.set_xlim(minx - pad, maxx + pad)
ax.set_ylim(miny - pad, maxy + pad)
fig.suptitle('Mesh Cell Comparison: Original vs Regenerated', fontsize=14, y=1.01)
plt.tight_layout()
plt.savefig(Path(project_folder) / 'fig2_before_after.png', dpi=150, bbox_inches='tight')
plt.show()

Figure 3: Zoomed Pipe Inlet Detail¶
Zoom into an area with pipe inlets to show the mesh cell polygons surrounding each pipe node.
# Find a cluster of pipe nodes near the center of the mesh
cx, cy = pipe_node_pts[:, 0].mean(), pipe_node_pts[:, 1].mean()
center_idx = np.argmin(np.sqrt((pipe_node_pts[:, 0] - cx)**2 + (pipe_node_pts[:, 1] - cy)**2))
zoom_x, zoom_y = pipe_node_pts[center_idx]
zoom_radius = 600 # ft
x0, x1 = zoom_x - zoom_radius, zoom_x + zoom_radius
y0, y1 = zoom_y - zoom_radius, zoom_y + zoom_radius
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7))
for ax, polys, title, fc, ec in [
(ax1, orig_area2, 'Original mesh', 'lightsteelblue', 'steelblue'),
(ax2, regen_area2, 'Regenerated mesh', 'navajowhite', 'darkorange'),
]:
# Spatial subset of cell polygons to the zoom window
sub = polys.cx[x0:x1, y0:y1]
sub.plot(ax=ax, facecolor=fc, edgecolor=ec, linewidth=0.6, alpha=0.6, zorder=1)
lc = LineCollection(conduit_segs, colors='gray', linewidths=1.0, alpha=0.7, zorder=2)
ax.add_collection(lc)
node_mask = (
(pipe_node_pts[:, 0] > x0) & (pipe_node_pts[:, 0] < x1) &
(pipe_node_pts[:, 1] > y0) & (pipe_node_pts[:, 1] < y1)
)
ax.scatter(pipe_node_pts[node_mask, 0], pipe_node_pts[node_mask, 1],
s=45, c='red', marker='s', edgecolors='black', linewidths=0.5,
label='Pipe nodes', zorder=3)
ax.set_xlim(x0, x1)
ax.set_ylim(y0, y1)
ax.set_title(title, fontsize=11)
ax.set_xlabel('Easting (ft)')
ax.set_ylabel('Northing (ft)')
ax.legend(loc='upper right', fontsize=9)
ax.set_aspect('equal')
ax.grid(alpha=0.3)
fig.suptitle('Zoomed Pipe Inlet Area: Pipe Nodes Within Mesh Cells', fontsize=13, y=1.01)
plt.tight_layout()
plt.savefig(Path(project_folder) / 'fig3_zoom_inlets.png', dpi=150, bbox_inches='tight')
plt.show()

Proof 1: Multi-Area Integrity¶
Confirm that only area2 was modified — DS Channel must be untouched. The counts below
are Storage Area 2D Points= seed counts from the .g## text; area2 keeps the same
2,705 seeds because regeneration reproduces them exactly.
post_text = new_geom_path.read_text(encoding='utf-8', errors='replace')
post_counts = {}
current_area = None
for line in post_text.splitlines():
if line.startswith('Storage Area='):
current_area = line.split('=', 1)[1].split(',')[0].strip()
elif line.startswith('Storage Area 2D Points=') and current_area:
post_counts[current_area] = int(line.split('=', 1)[1].strip())
integrity_df = pd.DataFrame([
{'Area': area, 'Before': pre_counts.get(area, 0),
'After': post_counts.get(area, 0),
'Changed': pre_counts.get(area, 0) != post_counts.get(area, 0)}
for area in pre_counts
])
print(integrity_df.to_string(index=False))
assert post_counts['DS Channel'] == pre_counts['DS Channel'], \
"FAIL: DS Channel was incorrectly modified!"
assert post_counts['area2'] == result.cell_count, \
"FAIL: area2 text count does not match generate() cell_count!"
print("\nMulti-area integrity: PASSED")
Area Before After Changed
area2 2705 2705 False
DS Channel 11 11 False
Multi-area integrity: PASSED
Proof 2: Pipe Network Tables Recomputed¶
Verify that pipe network connectivity tables exist in the regenerated HDF and reference valid cell/face indices.
with h5py.File(str(new_hdf_path), 'r') as f:
pn = f['Geometry/Pipe Networks/Davis']
face_tbl = pn['Face Property Table']
cell_tbl = pn['Cell Property Table']
node_conn = pn['Node Surface Connectivity']
cell_ids = pn['Cells Node and Conduit IDs']
post_face_shape = face_tbl.shape
post_cell_shape = cell_tbl.shape
post_node_shape = node_conn.shape
# Node Surface Connectivity is a compound dtype with fields:
# Node ID, Layer, Layer ID, Sublayer ID (cell index)
node_conn_data = node_conn[:]
sublayer_ids = node_conn_data['Sublayer ID']
n_connected = np.count_nonzero(sublayer_ids >= 0)
print("Pipe network tables (before -> after):")
print(f" Face table: {orig_face_shape} -> {post_face_shape}")
print(f" Cell table: {orig_cell_shape} -> {post_cell_shape}")
print(f" Node connectivity: {orig_node_shape} -> {post_node_shape}")
print(f" Nodes with surface connection: {n_connected} / {len(node_conn_data)}")
print(f" Cells with pipe connections: {cell_ids.shape[0]}")
# DS Channel cell count preserved in HDF
ds_cc = f['Geometry/2D Flow Areas/DS Channel/Cells Center Coordinate']
print(f"\n DS Channel cells in HDF: {ds_cc.shape[0]} (preserved)")
assert post_face_shape[0] > 0, "FAIL: Face table is empty!"
assert post_cell_shape[0] > 0, "FAIL: Cell table is empty!"
assert post_node_shape[0] == n_nodes, "FAIL: Node connectivity count mismatch!"
print("\nPipe network tables: PASSED")
Pipe network tables (before -> after):
Face table: (1991,) -> (1991,)
Cell table: (1992,) -> (1992,)
Node connectivity: (133,) -> (133,)
Nodes with surface connection: 133 / 133
Cells with pipe connections: 1992
DS Channel cells in HDF: 36 (preserved)
Pipe network tables: PASSED
Proof 3: Exact Cell Center Match¶
Verify the regenerated mesh cell centers are identical to the original — same count (2,941),
same positions, to machine precision. This proves RegenerateMeshPoints reproduces the same
seed points, so the HEC-RAS preprocessor rebuilds the identical computational mesh.
from scipy.spatial import cKDTree
# Compare all cell centers between original and regenerated HDF
with h5py.File(str(hdf_path), "r") as f:
orig_hdf_cc = f["Geometry/2D Flow Areas/area2/Cells Center Coordinate"][:]
with h5py.File(str(new_hdf_path), "r") as f:
regen_hdf_cc = f["Geometry/2D Flow Areas/area2/Cells Center Coordinate"][:]
print(f"Original HDF cells: {orig_hdf_cc.shape[0]:,}")
print(f"Regenerated HDF cells: {regen_hdf_cc.shape[0]:,}")
assert orig_hdf_cc.shape[0] == regen_hdf_cc.shape[0], "Cell count mismatch!"
tree_orig = cKDTree(orig_hdf_cc)
match_dists, _ = tree_orig.query(regen_hdf_cc)
print()
print("Nearest-neighbor distances:")
print(f" Max: {match_dists.max():.6f} ft")
print(f" Mean: {match_dists.mean():.6f} ft")
exact = np.sum(match_dists < 0.001)
print(f" Exact matches (<0.001 ft): {exact:,} / {len(match_dists):,}")
assert match_dists.max() < 0.01, "Cell positions differ!"
print()
print("Exact cell center match: PASSED")
exact_match = True
Original HDF cells: 2,941
Regenerated HDF cells: 2,941
Nearest-neighbor distances:
Max: 0.000000 ft
Mean: 0.000000 ft
Exact matches (<0.001 ft): 2,941 / 2,941
Exact cell center match: PASSED
Proof 4: HdfPipe API Coverage¶
Demonstrate that the ras-commander HdfPipe API reads pipe node attributes
and inlet data correctly, covering the full pipe network query surface.
from ras_commander.hdf.HdfPipe import HdfPipe
# Read pipe nodes with attributes via API
nodes_gdf = HdfPipe.get_pipe_nodes(str(new_hdf_path))
print(f"HdfPipe.get_pipe_nodes(): {len(nodes_gdf)} nodes")
print(f" Columns: {list(nodes_gdf.columns)}")
if len(nodes_gdf) > 0:
print(nodes_gdf.head(3).to_string())
# Read pipe conduits via API
conduits_gdf = HdfPipe.get_pipe_conduits(str(new_hdf_path))
print()
print(f"HdfPipe.get_pipe_conduits(): {len(conduits_gdf)} conduits")
print(f" Columns: {list(conduits_gdf.columns)}")
# Read pipe inlets via API
inlets_df = HdfPipe.get_pipe_inlets(str(new_hdf_path))
print()
print(f"HdfPipe.get_pipe_inlets(): {len(inlets_df)} inlets")
if len(inlets_df) > 0:
inlet_types = inlets_df['inlet_type'].value_counts().to_dict()
print(f" Types: {inlet_types}")
print(f" Columns: {list(inlets_df.columns)}")
# Verify counts match raw h5py reads
assert len(nodes_gdf) == n_nodes, "Node count mismatch"
assert len(conduits_gdf) == n_conds, "Conduit count mismatch"
top_count = len(inlets_df[inlets_df["inlet_type"] == "top"]) if len(inlets_df) > 0 else 0
side_count = len(inlets_df[inlets_df["inlet_type"] == "side"]) if len(inlets_df) > 0 else 0
assert top_count == n_top, "Top inlet count mismatch"
assert side_count == n_side, "Side inlet count mismatch"
print()
print("HdfPipe API coverage: PASSED")
HdfPipe.get_pipe_nodes(): 133 nodes
Columns: ['Name', 'System Name', 'Node Type', 'Node Status', 'Condtui Connections (US:DS)', 'Invert Elevation', 'Base Area', 'Terrain Elevation', 'Terrain Elevation Override', 'Depth', 'Top Inlet Type', 'Top Inlet Elevation', 'Side Inlet Type', 'Side Inlet Elevation', 'Total Connection Count', 'geometry']
Name System Name Node Type Node Status Condtui Connections (US:DS) Invert Elevation Base Area Terrain Elevation Terrain Elevation Override Depth Top Inlet Type Top Inlet Elevation Side Inlet Type Side Inlet Elevation Total Connection Count geometry
0 O14-di027 Davis Junction Junction -with top inlet 1:1 36.060001 36.0 39.860001 NaN 3.799999 Top Inlet 1 39.863369 NaN 2 POINT (6637926.8105 1964917.3197)
1 P11-DMH004 Davis Junction Junction -with top inlet 1:1 38.169998 36.0 48.720001 NaN 10.550003 Top Inlet 1 48.718811 NaN 2 POINT (6629444.6337 1963504.411)
2 O14-DMH005 Davis Junction Junction -with top inlet 1:1 31.559999 36.0 40.840000 NaN 9.280001 Top Inlet 1 40.843731 NaN 2 POINT (6637368.4974 1966084.5743)
HdfPipe.get_pipe_conduits(): 132 conduits
Columns: ['Name', 'System Name', 'US Node', 'DS Node', 'Modeling Approach', 'Conduit Length', 'Max Cell Length', 'Shape', 'Rise', 'Span', "Manning's n", 'US Offset', 'DS Offset', 'US Elevation', 'DS Elevation', 'Slope', 'US Entrance Loss Coefficient', 'DS Exit Loss Coefficient', 'US Backflow Loss Coefficient', 'DS Backflow Loss Coefficient', 'DS Gate Type', 'Major Group', 'Minor Group', 'DS Flap Gate', 'Polyline']
HdfPipe.get_pipe_inlets(): 5 inlets
Types: {'top': 4, 'side': 1}
Columns: ['Name', 'Weir Length', 'Weir Coef', 'Orifice Area', 'Orifice Coef', 'Surcharge Only', 'inlet_type', 'Inlet Shape', 'Rise', 'Span']
HdfPipe API coverage: PASSED
summary_data = [
["Mesh generation", result.status == "complete",
f"{hdf_cell_count:,} HDF cells ({result.cell_count:,} seeds), "
f"{result.face_count:,} faces, {result.iterations} iter"],
["Multi-area integrity", post_counts["DS Channel"] == pre_counts["DS Channel"],
f'DS Channel: {post_counts["DS Channel"]} pts (unchanged)'],
["Pipe tables recomputed", post_face_shape[0] > 0 and post_node_shape[0] == n_nodes,
f"Face: {post_face_shape}, Cell: {post_cell_shape}, Nodes: {post_node_shape}"],
["Exact cell center match", exact_match,
f"{orig_hdf_cc.shape[0]:,} cells, max dist {match_dists.max():.6f} ft"],
["HdfPipe API coverage", len(nodes_gdf) == n_nodes,
f"{len(nodes_gdf)} nodes, {len(conduits_gdf)} conduits, {len(inlets_df)} inlets"],
]
summary_df = pd.DataFrame(summary_data, columns=["Check", "Pass", "Detail"])
summary_df["Result"] = summary_df["Pass"].map({True: "PASS", False: "FAIL"})
print("=" * 80)
print("Davis Pipe Network Mesh Generation - Validation Summary")
print("=" * 80)
print(summary_df[["Check", "Result", "Detail"]].to_string(index=False))
print("=" * 80)
all_pass = summary_df["Pass"].all()
status = "ALL CHECKS PASSED" if all_pass else "SOME CHECKS FAILED"
print()
print(f"Overall: {status}")
================================================================================
Davis Pipe Network Mesh Generation - Validation Summary
================================================================================
Check Result Detail
Mesh generation PASS 2,941 HDF cells (2,705 seeds), 5,555 faces, 1 iter
Multi-area integrity PASS DS Channel: 11 pts (unchanged)
Pipe tables recomputed PASS Face: (1991,), Cell: (1992,), Nodes: (133,)
Exact cell center match PASS 2,941 cells, max dist 0.000000 ft
HdfPipe API coverage PASS 133 nodes, 132 conduits, 5 inlets
================================================================================
Overall: ALL CHECKS PASSED
Conclusion¶
This notebook proved that GeomMesh.generate() works correctly with pipe network geometries:
- Mesh generation succeeds — the pipe network does not interfere with mesh computation
- Pipe tables auto-update —
.NET geom.Save()recomputes face/cell/node connectivity for the pipe network against the new mesh topology - Multi-area safe — only the target flow area is modified; other areas are preserved
- Identical computational mesh — the regenerated seeds reproduce the same 2,941-cell HEC-RAS mesh, cell-for-cell, to machine precision
- HdfPipe API coverage — the regenerated pipe network (nodes, conduits, inlets) reads back correctly through the public API
Seeds vs Computational Cells¶
GeomMesh.generate() writes 2,705 seed points (Storage Area 2D Points=) to the .g##
text — this is RasMapper's NonVirtualCellCount. The HEC-RAS geometry preprocessor builds the
2,941-cell computational mesh from those seeds and stores it in the .g##.hdf. The two
numbers describe different stages of the same mesh; this notebook reports the HDF computational
count for consistency with the geometry HEC provided.
How Pipe Networks Interact with Mesh Generation¶
- Pipe inlets/outlets connect to the 2D mesh at specific cell locations
- These connections are resolved during HEC-RAS preprocessing, not during mesh generation
- When the mesh changes,
geom.Save()recomputes which cells the pipe nodes connect to - The pipe conduit geometry itself is independent of the 2D mesh