Skip to content

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) and DS 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's NonVirtualCellCount). For area2 this is 2,705.
  • Computational cells — the mesh the HEC-RAS geometry preprocessor builds from those seeds and stores in the .g##.hdf. For area2 this 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

  1. GeomMesh.generate() works on geometries with pipe network connections
  2. .NET geom.Save() automatically recomputes pipe network face/cell/node tables
  3. Multi-area geometries are handled correctly (only the target area is modified)
  4. Regeneration reproduces the identical HEC-RAS computational mesh — same 2,941 cells, same positions to machine precision
  5. The HdfPipe API reads the regenerated pipe network (nodes, conduits, inlets) correctly

Setup

Python
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__}")
Text Only
LOCAL SOURCE MODE: Loading from G:\GH\ras-commander/ras_commander


Loaded: G:\GH\ras-commander\ras_commander\__init__.py

Extract Project and Initialize

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

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

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

png

Clone Geometry and Regenerate Mesh

Clone the geometry so the original is preserved, then regenerate the mesh for area2.

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

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

png

Figure 3: Zoomed Pipe Inlet Detail

Zoom into an area with pipe inlets to show the mesh cell polygons surrounding each pipe node.

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

png

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.

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

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

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

Python
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")
Text Only
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
Python
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}")
Text Only
================================================================================
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:

  1. Mesh generation succeeds — the pipe network does not interfere with mesh computation
  2. Pipe tables auto-update.NET geom.Save() recomputes face/cell/node connectivity for the pipe network against the new mesh topology
  3. Multi-area safe — only the target flow area is modified; other areas are preserved
  4. Identical computational mesh — the regenerated seeds reproduce the same 2,941-cell HEC-RAS mesh, cell-for-cell, to machine precision
  5. 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
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.