Skip to content

Fixing Blocked Obstruction Overlaps with RasFixit

This notebook demonstrates using the RasFixit module to automatically detect and repair overlapping blocked obstructions in HEC-RAS geometry files.

Example Project: HCFCD M3 Model A120-00-00 (Harris County Flood Control District)

Overview

Problem: HEC-RAS geometry files sometimes contain overlapping or adjacent blocked obstructions, which cause model errors during geometry preprocessing.

Solution: RasFixit.fix_blocked_obstructions() automatically: 1. Detects overlapping/adjacent obstructions 2. Applies the elevation envelope algorithm (uses max elevation in overlap zones) 3. Inserts 0.02-unit gaps to prevent adjacency errors 4. Creates before/after PNG visualizations for engineering review 5. Creates timestamped backups

Key Principle: All fixes preserve hydraulic behavior by using the maximum (most restrictive) elevation in overlap zones.

1. Setup and Imports

Python
# =============================================================================
# DEVELOPMENT MODE TOGGLE
# =============================================================================
USE_LOCAL_SOURCE = True  # <-- TOGGLE THIS

from pathlib import Path  # Always import Path (needed throughout notebook)

if USE_LOCAL_SOURCE:
    import sys
    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:
    print("📦 PIP PACKAGE MODE: Loading installed ras-commander")

# Import ras-commander
from ras_commander import RasFixit, FixResults, FixMessage, FixAction
from ras_commander.geom.GeomCrossSection import GeomCrossSection
from ras_commander.fixit import log_parser

# Additional imports for this notebook
import pandas as pd
import numpy as np
import shutil

# Verify which version loaded
import ras_commander
print(f"✓ Loaded: {ras_commander.__file__}")
Text Only
📁 LOCAL SOURCE MODE: Loading from G:\GH\ras-commander/ras_commander


2026-05-08 11:26:07 - numexpr.utils - INFO - NumExpr defaulting to 8 threads.


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

Parameters

Configure these values to customize the notebook for your project.

Python
# =============================================================================
# PARAMETERS - Edit these to customize the notebook
# =============================================================================
from pathlib import Path

# Project Configuration
#PROJECT_NAME = "Clear Creek"      # Example project to extract  NOT USED IN THIS NOTEBOOK
RAS_VERSION = "7.0"               # HEC-RAS version (6.3, 6.5, 6.6, etc.)

# Geometry Settings
GEOM_NUMBER = "01"                # Geometry file number
RIVER = "White"                   # River name for cross section queries
REACH = "West Fork"               # Reach name
CROSS_SECTION = "10457.31"        # Cross section station (RS)
Python
# Download HCFCD M3 Model A (Clear Creek) containing A120-00-00 project
# This is a real-world project with known obstruction overlap issues
import zipfile
from ras_commander.sources.county import M3Model

# Step 1: Extract M3 Model 'A' (Clear Creek watershed)
print("Extracting M3 Model 'A' (Clear Creek)...")
m3_path = M3Model.extract_model('A')
print(f"✓ M3 Model extracted to: {m3_path}")

# Step 2: Find and extract A120-00-00 project ZIP
a120_zip = m3_path / "HEC-RAS" / "A120-00-00.zip"
a120_folder = m3_path / "HEC-RAS" / "A120-00-00"

if not a120_folder.exists():
    print(f"Extracting A120-00-00 project...")
    with zipfile.ZipFile(a120_zip, 'r') as zip_ref:
        zip_ref.extractall(a120_folder)
    print(f"✓ Project extracted to: {a120_folder}")
else:
    print(f"✓ Project already exists: {a120_folder}")

# Step 3: Set paths for this notebook
project_folder = a120_folder
geom_file = project_folder / "A120_00_00.g01"

print(f"\nProject folder: {project_folder}")
print(f"Geometry file exists: {geom_file.exists()}")
Text Only
2026-05-08 11:26:09 - ras_commander.sources.county.m3_model - INFO - ----- M3Model Extracting Model -----


2026-05-08 11:26:09 - ras_commander.sources.county.m3_model - INFO - Extracting model 'A' - Clear Creek


2026-05-08 11:26:09 - ras_commander.sources.county.m3_model - INFO - Model 'Clear Creek' already exists at G:\GH\ras-commander\examples\m3_models\Clear Creek


2026-05-08 11:26:09 - ras_commander.sources.county.m3_model - INFO - Use overwrite=True to re-download


Extracting M3 Model 'A' (Clear Creek)...
✓ M3 Model extracted to: G:\GH\ras-commander\examples\m3_models\Clear Creek
✓ Project already exists: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00

Project folder: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00
Geometry file exists: True
Python
# Detect overlaps without modifying the file
results = RasFixit.detect_obstruction_overlaps(geom_file)

print(f"Geometry File: {geom_file.name}")
print(f"Cross Sections Checked: {results.total_xs_checked}")
print(f"Cross Sections with Overlaps: {results.total_xs_fixed}")

if results.messages:
    print(f"\nAffected cross sections:")
    for msg in results.messages:
        print(f"  RS {msg.station}: {msg.original_count} obstructions -> would become {msg.fixed_count}")
Text Only
2026-05-08 11:26:09 - ras_commander.geom.GeomCrossSection - WARNING - Expected 2 blocked obstructions, parsed 1


2026-05-08 11:26:09 - ras_commander.geom.GeomCrossSection - WARNING - Expected 3 blocked obstructions, parsed 2


Geometry File: A120_00_00.g01
Cross Sections Checked: 91
Cross Sections with Overlaps: 15

Affected cross sections:
  RS 20262.89: 5 obstructions -> would become 6
  RS 17853.49: 2 obstructions -> would become 2
  RS 17833.49: 2 obstructions -> would become 2
  RS 17612.53: 3 obstructions -> would become 3
  RS 15831.37: 2 obstructions -> would become 2
  RS 15136.2: 7 obstructions -> would become 8
  RS 14284.07: 3 obstructions -> would become 4
  RS 11422.31: 3 obstructions -> would become 4
  RS 11181.99: 3 obstructions -> would become 4
  RS 10150.51: 4 obstructions -> would become 5
  RS 10040.42: 4 obstructions -> would become 5
  RS 9960.36: 3 obstructions -> would become 4
  RS 5714.48: 2 obstructions -> would become 3
  RS 4894.121: 2 obstructions -> would become 2
  RS 4053.094: 2 obstructions -> would become 2

3. Examine the Overlapping Obstructions

Let's look at the detailed data for one of the affected cross sections.

Python
# Look at first affected cross section in detail
if results.messages:
    msg = results.messages[0]

    print(f"=== Cross Section RS {msg.station} ===")
    print(f"\nOriginal Obstructions ({msg.original_count}):")
    for i, (start, end, elev) in enumerate(msg.original_data, 1):
        print(f"  {i}. Station {start:8.2f} to {end:8.2f}, Elevation: {elev:.2f}")

    print(f"\nFixed Obstructions ({msg.fixed_count}) - using max elevation envelope:")
    for i, (start, end, elev) in enumerate(msg.fixed_data, 1):
        print(f"  {i}. Station {start:8.2f} to {end:8.2f}, Elevation: {elev:.2f}")
Text Only
=== Cross Section RS 20262.89 ===

Original Obstructions (5):
  1. Station  1771.46 to  2303.49, Elevation: 37.43
  2. Station  3165.57 to  4205.00, Elevation: 34.20
  3. Station  4825.70 to  5057.23, Elevation: 34.30
  4. Station  4850.00 to  5050.00, Elevation: 35.30
  5. Station  3100.00 to  3200.00, Elevation: 35.10

Fixed Obstructions (6) - using max elevation envelope:
  1. Station  1771.46 to  2303.49, Elevation: 37.43
  2. Station  3100.00 to  3200.00, Elevation: 35.10
  3. Station  3200.02 to  4205.00, Elevation: 34.20
  4. Station  4825.70 to  4850.00, Elevation: 34.30
  5. Station  4850.02 to  5050.00, Elevation: 35.30
  6. Station  5050.02 to  5057.23, Elevation: 34.30

4. Fix Overlapping Obstructions

Now let's apply the fixes. We'll work on a copy of the geometry file to preserve the original.

This will: 1. Create a timestamped backup of the file 2. Apply the elevation envelope algorithm to resolve overlaps 3. Generate before/after PNG visualizations 4. Return detailed results for engineering review

IMPORTANT: Always review the visualizations before accepting changes!

Python
# Create a working copy so we don't modify the original
working_copy = project_folder / "A120_00_00_test_copy.g01"
shutil.copy(geom_file, working_copy)
print(f"Created working copy: {working_copy.name}")

# Fix overlapping obstructions with visualization
fix_results = RasFixit.fix_blocked_obstructions(
    working_copy,
    backup=True,       # Create timestamped backup
    visualize=True     # Generate before/after PNGs
)

print(f"\n=== Fix Results ===")
print(f"Cross Sections Checked: {fix_results.total_xs_checked}")
print(f"Cross Sections Fixed: {fix_results.total_xs_fixed}")
print(f"Backup Created: {fix_results.backup_path}")
print(f"Visualizations: {fix_results.visualization_folder}")
Text Only
2026-05-08 11:26:09 - ras_commander.fixit.RasFixit - INFO - Created backup: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01.backup_20260508_112609


2026-05-08 11:26:09 - ras_commander.fixit.RasFixit - INFO - Saving visualizations to: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy_g01_Obstructions_Fixed


Created working copy: A120_00_00_test_copy.g01


2026-05-08 11:26:09 - ras_commander.geom.GeomCrossSection - WARNING - Expected 2 blocked obstructions, parsed 1


2026-05-08 11:26:09 - ras_commander.geom.GeomCrossSection - WARNING - Expected 3 blocked obstructions, parsed 2


2026-05-08 11:26:09 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:09 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 20262.89: 6 obstructions written


2026-05-08 11:26:09 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:09 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 17853.49: 2 obstructions written


2026-05-08 11:26:10 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:10 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 17833.49: 2 obstructions written


2026-05-08 11:26:10 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:10 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 17612.53: 3 obstructions written


2026-05-08 11:26:10 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:10 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 15831.37: 2 obstructions written


2026-05-08 11:26:11 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:11 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 15136.2: 8 obstructions written


2026-05-08 11:26:11 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:11 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 14284.07: 4 obstructions written


2026-05-08 11:26:11 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:11 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 11422.31: 4 obstructions written


2026-05-08 11:26:11 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:11 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 11181.99: 4 obstructions written


2026-05-08 11:26:12 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:12 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 10150.51: 5 obstructions written


2026-05-08 11:26:12 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:12 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 10040.42: 5 obstructions written


2026-05-08 11:26:12 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:12 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 9960.36: 4 obstructions written


2026-05-08 11:26:13 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:13 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 5714.48: 3 obstructions written


2026-05-08 11:26:13 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:13 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 4894.121: 2 obstructions written


2026-05-08 11:26:13 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01


2026-05-08 11:26:13 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 4053.094: 2 obstructions written


2026-05-08 11:26:13 - ras_commander.fixit.RasFixit - INFO - Fixed 15 cross sections in A120_00_00_test_copy.g01



=== Fix Results ===
Cross Sections Checked: 91
Cross Sections Fixed: 15
Backup Created: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy.g01.backup_20260508_112609
Visualizations: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy_g01_Obstructions_Fixed

5. Review Fix Results

The FixResults object contains detailed information about every fix applied. Let's examine it.

Python
# Convert results to DataFrame for easy analysis
df = fix_results.to_dataframe()
print(f"Fix Summary DataFrame ({len(df)} rows):")
print(df[['station', 'action', 'original_count', 'fixed_count', 'message']].to_string())
Text Only
Fix Summary DataFrame (15 rows):
     station            action  original_count  fixed_count                                                   message
0   20262.89  OVERLAP_RESOLVED               5            6  Resolved 5 overlapping obstructions to 6 non-overlapping
1   17853.49  OVERLAP_RESOLVED               2            2  Resolved 2 overlapping obstructions to 2 non-overlapping
2   17833.49  OVERLAP_RESOLVED               2            2  Resolved 2 overlapping obstructions to 2 non-overlapping
3   17612.53  OVERLAP_RESOLVED               3            3  Resolved 3 overlapping obstructions to 3 non-overlapping
4   15831.37  OVERLAP_RESOLVED               2            2  Resolved 2 overlapping obstructions to 2 non-overlapping
5    15136.2  OVERLAP_RESOLVED               7            8  Resolved 7 overlapping obstructions to 8 non-overlapping
6   14284.07  OVERLAP_RESOLVED               3            4  Resolved 3 overlapping obstructions to 4 non-overlapping
7   11422.31  OVERLAP_RESOLVED               3            4  Resolved 3 overlapping obstructions to 4 non-overlapping
8   11181.99  OVERLAP_RESOLVED               3            4  Resolved 3 overlapping obstructions to 4 non-overlapping
9   10150.51  OVERLAP_RESOLVED               4            5  Resolved 4 overlapping obstructions to 5 non-overlapping
10  10040.42  OVERLAP_RESOLVED               4            5  Resolved 4 overlapping obstructions to 5 non-overlapping
11   9960.36  OVERLAP_RESOLVED               3            4  Resolved 3 overlapping obstructions to 4 non-overlapping
12   5714.48  OVERLAP_RESOLVED               2            3  Resolved 2 overlapping obstructions to 3 non-overlapping
13  4894.121  OVERLAP_RESOLVED               2            2  Resolved 2 overlapping obstructions to 2 non-overlapping
14  4053.094  OVERLAP_RESOLVED               2            2  Resolved 2 overlapping obstructions to 2 non-overlapping
Python
# Export to CSV for documentation
csv_path = project_folder / "obstruction_fix_report.csv"
df.to_csv(csv_path, index=False)
print(f"Fix report exported to: {csv_path}")
Text Only
Fix report exported to: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\obstruction_fix_report.csv

6. View Visualizations

The PNG visualizations show before/after comparisons for each fixed cross section. They are critical for engineering review.

Python
# Display a visualization (requires matplotlib)
import matplotlib.pyplot as plt
from matplotlib.image import imread

if fix_results.visualization_folder and fix_results.visualization_folder.exists():
    png_files = sorted(fix_results.visualization_folder.glob("*.png"))

    if png_files:
        # Display first visualization
        img = imread(png_files[0])
        plt.figure(figsize=(14, 10))
        plt.imshow(img)
        plt.axis('off')
        plt.title(f"Visualization: {png_files[0].name}")
        plt.tight_layout()
        plt.show()

        print(f"\nTotal visualizations generated: {len(png_files)}")
        print(f"Location: {fix_results.visualization_folder}")
    else:
        print("No PNG files found in visualization folder.")
else:
    print("No visualization folder available.")

png

Text Only
Total visualizations generated: 15
Location: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_test_copy_g01_Obstructions_Fixed
Python
# Display all visualizations (optional - may be slow for many cross sections)
show_all = False  # Set to True to view all

if show_all and fix_results.visualization_folder:
    png_files = sorted(fix_results.visualization_folder.glob("*.png"))

    for png_file in png_files:
        img = imread(png_file)
        plt.figure(figsize=(14, 10))
        plt.imshow(img)
        plt.axis('off')
        plt.title(png_file.name)
        plt.tight_layout()
        plt.show()

7. Verify Fixes

After fixing, verify that no overlaps remain.

Python
# Verify no overlaps remain after fix
verify_results = RasFixit.detect_obstruction_overlaps(working_copy)

if verify_results.total_xs_fixed == 0:
    print("SUCCESS: No overlapping obstructions remaining!")
    print(f"Verified {verify_results.total_xs_checked} cross sections.")
else:
    print(f"WARNING: {verify_results.total_xs_fixed} cross sections still have overlaps.")
    print("Please review the file manually.")
Text Only
2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - WARNING - Expected 2 blocked obstructions, parsed 1


2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - WARNING - Expected 3 blocked obstructions, parsed 2


SUCCESS: No overlapping obstructions remaining!
Verified 91 cross sections.

8. Log Parsing for Automated Workflows

The log_parser module can detect obstruction errors from HEC-RAS compute logs, enabling automated fix workflows.

Python
# Example: Parse HEC-RAS log for obstruction errors
sample_log = """
HEC-RAS Model Computation
Processing cross section 20262.89
ERROR: Cross Section 20262.89 has overlapping blocked obstructions
Processing cross section 18765.43
ERROR: Blocked obstructions at station 18765.43 overlap or are adjacent
Computation complete with errors.
"""

# Detect errors
errors = log_parser.detect_obstruction_errors(sample_log)
print(f"Detected {len(errors)} obstruction errors")

# Extract affected stations
stations = log_parser.extract_cross_section_ids(sample_log)
print(f"Affected stations: {stations}")

# Generate human-readable report
print("\n" + log_parser.generate_error_report(errors))
Text Only
Detected 2 obstruction errors
Affected stations: ['18765.43', '20262.89']

================================================================================
BLOCKED OBSTRUCTION ERROR REPORT
================================================================================

Total Errors Found: 2


OVERLAP (1 occurrences):
--------------------------------------------------------------------------------
  River Station: 20262.89
  Line: 4
  Message: ERROR: Cross Section 20262.89 has overlapping blocked obstructions


ADJACENT (1 occurrences):
--------------------------------------------------------------------------------
  River Station: 18765.43
  Line: 6
  Message: ERROR: Blocked obstructions at station 18765.43 overlap or are adjacent

================================================================================
Python
# Automated workflow: Detect errors from log, then fix geometry files
def auto_fix_workflow(log_file_path, project_dir):
    """Automated workflow to detect and fix obstruction errors."""

    # Step 1: Check log for errors
    if not log_parser.has_obstruction_errors(log_file_path):
        print("No obstruction errors found in log.")
        return None

    # Step 2: Read log and detect errors
    with open(log_file_path, 'r') as f:
        log_content = f.read()

    errors = log_parser.detect_obstruction_errors(log_content)
    print(f"Found {len(errors)} obstruction errors")

    # Step 3: Find geometry files in project
    geom_files = log_parser.find_geometry_files_in_directory(project_dir)
    print(f"Found {len(geom_files)} geometry files")

    # Step 4: Fix each geometry file
    all_results = {}
    for geom_path in geom_files:
        results = RasFixit.fix_blocked_obstructions(
            geom_path,
            backup=True,
            visualize=True
        )
        all_results[geom_path] = results
        print(f"  Fixed {results.total_xs_fixed} cross sections in {Path(geom_path).name}")

    return all_results

print("auto_fix_workflow() function defined.")
print("Usage: auto_fix_workflow('compute.log', 'path/to/project')")
Text Only
auto_fix_workflow() function defined.
Usage: auto_fix_workflow('compute.log', 'path/to/project')

9. GeomCrossSection Blocked Obstruction API

The GeomCrossSection class exposes low-level read/write methods for blocked obstructions, independent of the RasFixit repair workflow. Use these when you need to:

  • Inspect obstruction data across all cross sections as a DataFrame
  • Programmatically write new or modified obstructions to a .g## file
  • Cross-validate text-parsed obstructions against HDF geometry flags

These methods follow ras-commander API conventions: @staticmethod, @log_call, and accept either a geometry number with ras_object or a direct file path.

9a. Read Blocked Obstructions as a DataFrame

get_blocked_obstructions() returns a DataFrame with xs_id, River, Reach, RS, obstruction_index, start_sta, end_sta, and elevation columns. Pass no xs_id to read all cross sections, or pass a specific identifier to filter.

Python
# Read all blocked obstructions from the original geometry file
all_obs = GeomCrossSection.get_blocked_obstructions(geom_file)

print(f"Total obstructions across all cross sections: {len(all_obs)}")
print(f"Cross sections with obstructions: {all_obs['xs_id'].nunique()}")
print(f"\nColumns: {list(all_obs.columns)}")
print(f"\nFirst 10 rows:")
all_obs.head(10)
Text Only
2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - WARNING - Expected 2 blocked obstructions, parsed 1


2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - WARNING - Expected 3 blocked obstructions, parsed 2


Total obstructions across all cross sections: 178
Cross sections with obstructions: 72

Columns: ['xs_id', 'River', 'Reach', 'RS', 'obstruction_index', 'start_sta', 'end_sta', 'elevation']

First 10 rows:
xs_id River Reach RS obstruction_index start_sta end_sta elevation
0 A120-00-00|A120-00-00_0008|29113.3 A120-00-00 A120-00-00_0008 29113.3 0 5240.00 5875.00 40.5
1 A120-00-00|A120-00-00_0008|28913.21 A120-00-00 A120-00-00_0008 28913.21 0 5193.17 5481.91 41.0
2 A120-00-00|A120-00-00_0008|28022.89 A120-00-00 A120-00-00_0008 28022.89 0 3200.00 3400.00 41.0
3 A120-00-00|A120-00-00_0008|28022.89 A120-00-00 A120-00-00_0008 28022.89 1 3571.00 4500.00 39.6
4 A120-00-00|A120-00-00_0008|28022.89 A120-00-00 A120-00-00_0008 28022.89 2 5200.00 5900.00 41.0
5 A120-00-00|A120-00-00_0008|27857.47 A120-00-00 A120-00-00_0008 27857.47 0 2900.00 3150.00 41.0
6 A120-00-00|A120-00-00_0008|27857.47 A120-00-00 A120-00-00_0008 27857.47 1 3151.00 4300.00 39.5
7 A120-00-00|A120-00-00_0008|26578.6 A120-00-00 A120-00-00_0008 26578.6 0 -500.00 -180.00 41.0
8 A120-00-00|A120-00-00_0008|26578.6 A120-00-00 A120-00-00_0008 26578.6 1 -150.00 2300.00 39.4
9 A120-00-00|A120-00-00_0008|25646.24 A120-00-00 A120-00-00_0008 25646.24 0 -350.00 0.00 40.5
Python
# Read obstructions for a single cross section using xs_id
target_xs = all_obs["xs_id"].iloc[0]
single_xs_obs = GeomCrossSection.get_blocked_obstructions(geom_file, xs_id=target_xs)

print(f"Obstructions for {target_xs}:")
for _, row in single_xs_obs.iterrows():
    print(f"  [{row['obstruction_index']}] Station {row['start_sta']:8.2f} to {row['end_sta']:8.2f}, Elevation: {row['elevation']:.2f}")

# Summary statistics
print(f"\nSummary across all cross sections:")
summary = all_obs.groupby("xs_id").agg(
    count=("obstruction_index", "count"),
    min_sta=("start_sta", "min"),
    max_sta=("end_sta", "max"),
    min_elev=("elevation", "min"),
    max_elev=("elevation", "max"),
).reset_index()
summary
Text Only
Obstructions for A120-00-00|A120-00-00_0008|29113.3:
  [0] Station  5240.00 to  5875.00, Elevation: 40.50

Summary across all cross sections:
xs_id count min_sta max_sta min_elev max_elev
0 A120-00-00|A120-00-00_0008|10040.42 4 1685.00 9522.18 30.95 32.50
1 A120-00-00|A120-00-00_0008|10150.51 4 898.75 9450.97 31.00 32.67
2 A120-00-00|A120-00-00_0008|10170.53 4 1205.92 9350.00 31.00 32.99
3 A120-00-00|A120-00-00_0008|11181.99 3 1200.00 11118.41 31.25 32.70
4 A120-00-00|A120-00-00_0008|11422.31 3 1200.00 11836.18 31.35 33.10
... ... ... ... ... ... ...
67 A120-00-00|A120-00-00_0008|9291.765 2 2650.00 8000.00 30.65 30.65
68 A120-00-00|A120-00-00_0008|9331.771 3 2350.00 8700.00 30.70 30.70
69 A120-00-00|A120-00-00_0008|9392.3 4 2450.00 9608.65 30.80 32.67
70 A120-00-00|A120-00-00_0008|9402.438 5 2500.00 9900.00 30.80 32.85
71 A120-00-00|A120-00-00_0008|9960.36 3 1730.00 9397.04 30.90 32.70

72 rows × 6 columns

9b. Write Blocked Obstructions (Round-Trip)

set_blocked_obstructions() writes obstruction data to a specific cross section. It accepts BlockedObstruction objects, DataFrames, dicts, or (start_sta, end_sta, elevation) tuples. A backup is created by default.

Here we demonstrate a read-modify-write round trip: read the original obstructions, shift elevations up by 1 foot, write them back, and verify the round trip.

Python
# Create a fresh working copy for the write demo
write_demo_copy = project_folder / "A120_00_00_write_demo.g01"
shutil.copy(geom_file, write_demo_copy)

# Read original obstructions for a specific cross section
target_xs = all_obs["xs_id"].iloc[0]
original = GeomCrossSection.get_blocked_obstructions(write_demo_copy, xs_id=target_xs)
print(f"Original obstructions for {target_xs}:")
print(original[["start_sta", "end_sta", "elevation"]].to_string(index=False))

# Modify: shift all elevations up by 1 foot
modified = original[["start_sta", "end_sta", "elevation"]].copy()
modified["elevation"] = modified["elevation"] + 1.0

print(f"\nModified obstructions (elevation + 1.0 ft):")
print(modified.to_string(index=False))

# Write the modified obstructions back
backup_path = GeomCrossSection.set_blocked_obstructions(
    write_demo_copy,
    xs_id=target_xs,
    obstructions=modified,
)
print(f"\nBackup created: {backup_path.name}")
Text Only
Original obstructions for A120-00-00|A120-00-00_0008|29113.3:


2026-05-08 11:26:14 - ras_commander.geom.GeomParser - INFO - Created backup: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_write_demo.g01.bak


 start_sta  end_sta  elevation
    5240.0   5875.0       40.5

Modified obstructions (elevation + 1.0 ft):
 start_sta  end_sta  elevation
    5240.0   5875.0       41.5


2026-05-08 11:26:14 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_write_demo.g01


2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 29113.3: 1 obstructions written



Backup created: A120_00_00_write_demo.g01.bak
Python
# Verify the round trip: re-read and compare
roundtrip = GeomCrossSection.get_blocked_obstructions(write_demo_copy, xs_id=target_xs)

print("Round-trip verification:")
print(roundtrip[["start_sta", "end_sta", "elevation"]].to_string(index=False))

# Check values match
assert np.allclose(roundtrip["start_sta"].values, modified["start_sta"].values), "start_sta mismatch"
assert np.allclose(roundtrip["end_sta"].values, modified["end_sta"].values), "end_sta mismatch"
assert np.allclose(roundtrip["elevation"].values, modified["elevation"].values), "elevation mismatch"
print(f"\n✓ Round trip verified: {len(roundtrip)} obstructions match")

# Verify other cross sections were not affected
all_obs_after = GeomCrossSection.get_blocked_obstructions(write_demo_copy)
other_xs_before = all_obs[all_obs["xs_id"] != target_xs]
other_xs_after = all_obs_after[all_obs_after["xs_id"] != target_xs]

assert len(other_xs_before) == len(other_xs_after), "Other XS obstruction count changed!"
assert np.allclose(
    other_xs_before["elevation"].values,
    other_xs_after["elevation"].values,
), "Other XS elevations changed!"
print(f"✓ Other cross sections unaffected ({len(other_xs_after)} obstructions unchanged)")
Text Only
2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - WARNING - Expected 2 blocked obstructions, parsed 1


2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - WARNING - Expected 3 blocked obstructions, parsed 2


Round-trip verification:
 start_sta  end_sta  elevation
    5240.0   5875.0       41.5

✓ Round trip verified: 1 obstructions match
✓ Other cross sections unaffected (177 obstructions unchanged)

9c. Insert New Obstructions and Remove Existing Ones

set_blocked_obstructions() can also insert a new obstruction block into a cross section that has none, or remove all obstructions by passing an empty list.

Python
# Find a cross section that has NO obstructions
xs_df = GeomCrossSection.get_cross_sections(write_demo_copy)
xs_with_obs = set(all_obs["xs_id"].unique())
all_xs_ids = [
    GeomCrossSection._make_xs_id(row["River"], row["Reach"], row["RS"])
    for _, row in xs_df.iterrows()
]
xs_without_obs = [xid for xid in all_xs_ids if xid not in xs_with_obs]

if xs_without_obs:
    insert_target = xs_without_obs[0]
    print(f"Cross section with no obstructions: {insert_target}")

    # Insert new obstructions using tuples
    new_obs = [
        (100.0, 200.0, 50.0),
        (300.0, 400.0, 55.0),
    ]
    GeomCrossSection.set_blocked_obstructions(
        write_demo_copy, xs_id=insert_target, obstructions=new_obs, create_backup=False,
    )

    # Verify insertion
    inserted = GeomCrossSection.get_blocked_obstructions(write_demo_copy, xs_id=insert_target)
    print(f"Inserted {len(inserted)} obstructions:")
    print(inserted[["start_sta", "end_sta", "elevation"]].to_string(index=False))

    # Now remove them by passing an empty list
    GeomCrossSection.set_blocked_obstructions(
        write_demo_copy, xs_id=insert_target, obstructions=[], create_backup=False,
    )
    removed = GeomCrossSection.get_blocked_obstructions(write_demo_copy, xs_id=insert_target)
    print(f"\nAfter removal: {len(removed)} obstructions (block deleted from file)")
else:
    print("All cross sections have obstructions in this geometry.")
Text Only
2026-05-08 11:26:14 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_write_demo.g01


2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 25548: 2 obstructions written


2026-05-08 11:26:14 - ras_commander.geom.GeomParser - INFO - Successfully wrote geometry file: G:\GH\ras-commander\examples\m3_models\Clear Creek\HEC-RAS\A120-00-00\A120_00_00_write_demo.g01


2026-05-08 11:26:14 - ras_commander.geom.GeomCrossSection - INFO - Updated blocked obstructions for A120-00-00/A120-00-00_0008/RS 25548: 0 obstructions written


Cross section with no obstructions: A120-00-00|A120-00-00_0008|25548
Inserted 2 obstructions:
 start_sta  end_sta  elevation
     100.0    200.0       50.0
     300.0    400.0       55.0

After removal: 0 obstructions (block deleted from file)

9d. HDF Cross-Validation

validate_blocked_obstructions_hdf() compares text-parsed #Block Obstruct= presence against the HDF geometry's Obstr Block Mode flag per cross section. This catches text/HDF drift after manual edits.

Note: This requires a preprocessed .g##.hdf file alongside the text geometry. The M3 model used in this notebook does not include a preprocessed HDF, so we show the API signature below.

Python
# HDF cross-validation (requires preprocessed .g##.hdf file)
hdf_file = Path(str(geom_file) + ".hdf")

if hdf_file.exists():
    validation = GeomCrossSection.validate_blocked_obstructions_hdf(geom_file)
    mismatches = validation[~validation["matches_hdf"]]
    print(f"Validated {len(validation)} cross sections against HDF")
    print(f"Mismatches: {len(mismatches)}")
    if len(mismatches) > 0:
        print(mismatches[["xs_id", "text_obstruction_count", "hdf_obstr_block_mode", "matches_hdf"]])
    else:
        print("All text/HDF obstruction flags agree.")
else:
    print(f"No HDF file found at {hdf_file.name}")
    print("To use HDF validation, preprocess the geometry in HEC-RAS first.")
    print()
    print("Usage:")
    print("  validation = GeomCrossSection.validate_blocked_obstructions_hdf(geom_file)")
    print("  validation = GeomCrossSection.validate_blocked_obstructions_hdf(geom_file, hdf_path='custom.hdf')")
    print()
    print("Returns a DataFrame with columns:")
    print("  xs_id, River, Reach, RS, text_obstruction_count,")
    print("  text_has_blocked_obstructions, hdf_obstr_block_mode,")
    print("  hdf_has_blocked_obstructions, matches_hdf")
Text Only
No HDF file found at A120_00_00.g01.hdf
To use HDF validation, preprocess the geometry in HEC-RAS first.

Usage:
  validation = GeomCrossSection.validate_blocked_obstructions_hdf(geom_file)
  validation = GeomCrossSection.validate_blocked_obstructions_hdf(geom_file, hdf_path='custom.hdf')

Returns a DataFrame with columns:
  xs_id, River, Reach, RS, text_obstruction_count,
  text_has_blocked_obstructions, hdf_obstr_block_mode,
  hdf_has_blocked_obstructions, matches_hdf

10. Cleanup (Optional)

Remove test files after verification.

Python
# Optional: Clean up test files
cleanup = True  # Set to False to keep test files

if cleanup:
    # Remove working copy
    if working_copy.exists():
        working_copy.unlink()
        print(f"Removed: {working_copy.name}")

    # Remove backup
    if fix_results.backup_path and fix_results.backup_path.exists():
        fix_results.backup_path.unlink()
        print(f"Removed backup: {fix_results.backup_path.name}")

    # Remove visualization folder
    if fix_results.visualization_folder and fix_results.visualization_folder.exists():
        shutil.rmtree(fix_results.visualization_folder)
        print(f"Removed visualization folder: {fix_results.visualization_folder.name}")

    # Remove CSV report
    if csv_path.exists():
        csv_path.unlink()
        print(f"Removed: {csv_path.name}")

    # Remove write demo copy and its backup
    if write_demo_copy.exists():
        write_demo_copy.unlink()
        print(f"Removed: {write_demo_copy.name}")
    for bak in project_folder.glob("A120_00_00_write_demo.g01.bak*"):
        bak.unlink()
        print(f"Removed backup: {bak.name}")

    print("\nCleanup complete!")
else:
    print("Cleanup skipped. Set cleanup=True to remove test files.")
Text Only
Removed: A120_00_00_test_copy.g01
Removed backup: A120_00_00_test_copy.g01.backup_20260508_112609
Removed visualization folder: A120_00_00_test_copy_g01_Obstructions_Fixed
Removed: obstruction_fix_report.csv
Removed: A120_00_00_write_demo.g01
Removed backup: A120_00_00_write_demo.g01.bak

Cleanup complete!

Summary

This notebook demonstrated:

Detection & Repair (RasFixit): - RasFixit.detect_obstruction_overlaps() - Non-destructive scan for overlaps - RasFixit.fix_blocked_obstructions() - Apply elevation envelope algorithm - backup=True creates timestamped backups - visualize=True generates before/after PNGs - dry_run=True detects without modifying

Low-Level Read/Write (GeomCrossSection): - GeomCrossSection.get_blocked_obstructions() - Read obstructions as DataFrame (all XS or filtered) - GeomCrossSection.set_blocked_obstructions() - Write/update/insert/remove obstructions per XS - GeomCrossSection.validate_blocked_obstructions_hdf() - Cross-check text vs HDF flags

Log Parsing: - log_parser.detect_obstruction_errors() - Parse HEC-RAS logs - log_parser.find_geometry_files_in_directory() - Find .g## files - log_parser.generate_error_report() - Human-readable report

Results: - FixResults.to_dataframe() - Export to pandas DataFrame - FixMessage contains original and fixed data for audit trail

Engineering Review Requirements

IMPORTANT: All fixes should be reviewed by a licensed professional engineer before accepting changes to production models.

The visualization outputs provide an audit trail showing: - Original obstruction configuration - Fixed obstruction configuration - Algorithm decisions made

Algorithm Details

The elevation envelope algorithm: 1. Collects all critical stations (start/end of each obstruction) 2. For each segment between stations, uses maximum elevation (most restrictive) 3. Merges adjacent segments with same elevation 4. Inserts 0.02-unit gaps where different elevations meet (HEC-RAS requirement)

This is hydraulically conservative - it preserves flow restrictions in overlap zones.

Example Project

This notebook uses the HCFCD M3 Model A120-00-00 (Harris County Flood Control District), which contains real-world blocked obstruction issues in 15 of its 91 cross sections.

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.