Project Structure (files included):
├── .gitattributes
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── NonWorking_Tools.md
├── README.md
├── TRADEMARKS.md
├── __main__.py
├── claude_desktop_config.json
├── package.json
├── pyproject.toml
├── ras-commander reference files
│   ├── HdfPlan.py
│   ├── HdfResultsPlan.py
│   ├── HdfResultsXsec.py
│   ├── RasPrj.py
│   └── api.md
├── requirements.txt
└── server.py

File: c:\GH\ras-commander-mcp-main\.gitattributes
==================================================
# Auto detect text files and perform LF normalization
* text=auto

==================================================

File: c:\GH\ras-commander-mcp-main\.gitignore
==================================================
/testdata
/__pycache__
/tests/outputs
uv.lock

==================================================

File: c:\GH\ras-commander-mcp-main\CLAUDE.md
==================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

**You prefer to read full file contents before editing**

## Commands

### Build & Development
- **Install dependencies**: `uv pip install mcp ras-commander pandas` or `uv sync`
- **Run MCP server**: `uv run python server.py`
- **Alternative for testing**: `uvx --from . ras-commander-mcp`

### Testing & Validation
- **Comprehensive testing**: `uv run python tests/test_all_tools.py` - Tests all MCP tools on both Muncie and BeaverLake projects
- **Single tool testing**: `uv run python tests/test_single_tool.py` - Modify the script to test specific tools quickly
- **Direct server testing**: `uv run python tests/test_server.py` - Basic functionality validation
- **Test data locations**: `testdata/Muncie/` and `testdata/BeaverLake/` contain complete HEC-RAS projects
- **Test outputs**: Check `tests/outputs/` for markdown reports of tool functionality
- **Integration testing**: Configure in Claude Desktop and test with actual HEC-RAS queries

### Required Post-Update Testing
After making changes to the MCP server, run these tests in order:
1. `uv run python tests/test_server.py` - Verify basic server functionality
2. `uv run python tests/test_all_tools.py` - Comprehensive tool validation
3. Review output files in `tests/outputs/` directory
4. Test Claude Desktop integration with sample queries

## Project Structure

```
ras-commander-mcp-main/
├── server.py                    # Main MCP server implementation
├── pyproject.toml              # Package configuration with dependencies
├── requirements.txt            # Legacy pip dependencies (for compatibility)
├── claude_desktop_config.json  # Example Claude Desktop configuration
├── tests/                      # Testing suite
│   ├── __init__.py
│   ├── test_server.py         # Basic server functionality tests
│   ├── test_all_tools.py      # Comprehensive tool testing suite
│   ├── test_single_tool.py    # Single tool testing utility
│   └── outputs/               # Test output markdown files
├── testdata/
│   ├── Muncie/                # Complete HEC-RAS project for testing
│   └── BeaverLake/            # Additional test project
├── ras-commander reference files/ # API documentation and examples
└── NonWorking_Tools.md         # Documentation of removed/broken tools
```

## Architecture

### MCP Server Implementation
This repository implements a Model Context Protocol (MCP) server that bridges HEC-RAS hydraulic modeling software with Claude Desktop. The server is built on top of the [ras-commander](https://github.com/gpt-cmdr/ras-commander) Python library.

**Core MCP Tools** (server.py:136-280):
- `hecras_project_summary`: Comprehensive or selective project information with boolean flags controlling output sections
- `read_plan_description`: Extract the multi-line description from a specific plan file
- `get_plan_results_summary`: Detailed plan results including unsteady simulation info, volume accounting, and runtime performance data
- `get_compute_messages`: Computation messages and performance metrics from HEC-RAS simulations with smart truncation
- `get_hdf_structure`: HDF file structure exploration with group/dataset details and attributes
- `get_projection_info`: Spatial projection information (WKT format) extraction from HDF files

**Data Processing Pipeline**:
1. HEC-RAS project files (.prj, .g*, .p*, .u*, etc.) 
2. ras-commander library parsing → pandas DataFrames
3. DataFrame filtering and formatting (server.py:66-135)
4. Text conversion with truncation limits (server.py:50-65)
5. Structured output to Claude via MCP protocol

**ras-commander Integration**:
- Uses `ras_commander.init_ras_project()` for project initialization
- Leverages `HdfBase`, `HdfResultsPlan` classes for HDF data access
- Implements local `get_compute_messages_local()` for simulation performance analysis
- Plan identification supports both numeric ("1", "01") and full HDF path inputs with auto-padding
- Follows ras-commander naming conventions and API patterns
- Supports HEC-RAS versions 6.5, 6.6 (configurable via environment variables)

### Key Dependencies
- **mcp**: Model Context Protocol server framework
- **ras-commander**: HEC-RAS project interface library (requires HEC-RAS installation)
- **pandas**: DataFrame handling and data manipulation
- **h5py**: Direct HDF5 file access for structure exploration

### Error Handling & Robustness
- Path validation before project initialization (server.py:272-276)
- Graceful handling of missing data components (plans, geometries, boundaries)
- Detailed error messages for common issues (version mismatches, missing files)
- Output truncation to prevent token limit exceeded errors (server.py:50-65)
- DataFrame column filtering for concise vs verbose output modes (server.py:66-135)

## HEC-RAS Integration Requirements
- HEC-RAS installation required at standard location: `C:\Program Files (x86)\HEC\HEC-RAS\`
- Project paths must point to folders containing `.prj` files
- Test projects (Muncie, BeaverLake) include complete model components
- Environment variables: `HECRAS_VERSION` (default: "6.6"), `HECRAS_PATH` (custom installation path)

## About This Tool
**RAS Commander MCP** is an open-source, LLM-forward H&H automation tool provided under MIT license by CLB Engineering Corporation. This is third-party software and is not made by or endorsed by USACE HEC. For more Python functionality, see the [ras-commander](https://github.com/gpt-cmdr/ras-commander) repository.
==================================================

File: c:\GH\ras-commander-mcp-main\claude_desktop_config.json
==================================================
{
  "mcpServers": {
    "hecras": {
      "command": "uvx",
      "args": [
        "--from", "ras-commander-mcp@git+https://github.com/gpt-cmdr/ras-commander-mcp.git",
        "ras-commander-mcp"
      ],
      "env": {
        "HECRAS_VERSION": "6.6"
      }
    }
  }
}
==================================================

File: c:\GH\ras-commander-mcp-main\LICENSE
==================================================
MIT License

Copyright (c) 2025 HEC-RAS MCP

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
==================================================

File: c:\GH\ras-commander-mcp-main\NonWorking_Tools.md
==================================================
# Non-Working Tools

This document contains tools that have been removed from the HEC-RAS MCP server due to issues or lack of proper functionality.

## get_infiltration_data Tool

**Status**: Removed  
**Reason**: Tool consistently returns "No infiltration layer file found in project" even for projects that should contain infiltration data. The issue appears to be related to how infiltration layer filenames are stored or referenced in the RASMapper configuration files.

### Original Tool Definition

```python
Tool(
    name="get_infiltration_data",
    description="Get infiltration layer data and soil statistics from a HEC-RAS project",
    inputSchema={
        "type": "object",
        "properties": {
            "project_path": {
                "type": "string",
                "description": "Full path to the HEC-RAS project folder"
            },
            "significant_threshold": {
                "type": "number",
                "description": "Minimum percentage threshold for significant mukeys",
                "default": 1.0
            }
        },
        "required": ["project_path"]
    }
)
```

### Original Implementation Code

```python
elif name == "get_infiltration_data":
    try:
        project_path = Path(arguments["project_path"])
        significant_threshold = arguments.get("significant_threshold", 1.0)
        
        if not project_path.exists() or not project_path.is_dir():
            return [TextContent(
                type="text",
                text=f"Error: The specified project folder does not exist: {project_path}"
            )]
        
        # Initialize project to get rasmap_df
        ras_version = HECRAS_PATH if HECRAS_PATH else HECRAS_VERSION
        ras = init_ras_project(project_path, ras_version)
        
        response_parts = [
            f"HEC-RAS Project: {ras.project_name}",
            f"Project Path: {project_path}",
            get_ras_version_info(),
            "=" * 80
        ]
        
        # Check if infiltration data exists in rasmap_df
        if hasattr(ras, 'rasmap_df') and ras.rasmap_df is not None and not ras.rasmap_df.empty:
            # Look for infiltration layer path
            infiltration_path = None
            if 'InfiltrationLayerFilename' in ras.rasmap_df.columns:
                infiltration_files = ras.rasmap_df['InfiltrationLayerFilename'].dropna()
                if not infiltration_files.empty:
                    infiltration_path = Path(project_path) / infiltration_files.iloc[0]
            
            if infiltration_path and infiltration_path.exists():
                # Get infiltration data
                infiltration_data = HdfInfiltration.get_infiltration_layer_data(infiltration_path)
                if infiltration_data is not None:
                    response_parts.append(dataframe_to_text(infiltration_data, "INFILTRATION LAYER DATA"))
                    
                    # Get significant mukeys if soil stats available
                    if 'percentage' in infiltration_data.columns:
                        significant_mukeys = HdfInfiltration.get_significant_mukeys(
                            infiltration_data, threshold=significant_threshold
                        )
                        if not significant_mukeys.empty:
                            response_parts.append(dataframe_to_text(
                                significant_mukeys, 
                                f"SIGNIFICANT MUKEYS (>{significant_threshold}%)"
                            ))
                            total_percentage = HdfInfiltration.calculate_total_significant_percentage(
                                significant_mukeys
                            )
                            response_parts.append(f"\nTotal significant percentage: {total_percentage:.2f}%")
                else:
                    response_parts.append("\nNo infiltration data found in layer file")
            else:
                response_parts.append("\nNo infiltration layer file found in project")
        else:
            response_parts.append("\nNo RASMapper configuration found")
        
        return [TextContent(
            type="text",
            text=truncate_output("\n".join(response_parts))
        )]
        
    except Exception as e:
        logger.error(f"Error getting infiltration data: {str(e)}")
        return [TextContent(
            type="text",
            text=f"Error getting infiltration data: {str(e)}"
        )]
```

### Required Import

The tool required this import in the server.py file:

```python
from ras_commander import init_ras_project, HdfBase, HdfInfiltration, HdfResultsPlan
```

### Original Test Code

The following test code was used in `tests/test_all_tools.py`:

```python
# Test 6: Get infiltration data (use project with infiltration data)
# Try multiple possible paths for BaldEagleCrkMulti2D project
infiltration_paths = [
    Path("C:\\GH\\ras-commander-mappingbranch\\examples\\example_projects\\BaldEagleCrkMulti2D"),
    Path("C:\\GH\\Claude_Code_Execution\\BaldEagleCrkMulti2D")
]

infiltration_project_path = None
for path in infiltration_paths:
    if path.exists():
        infiltration_project_path = path
        break

if infiltration_project_path:
    await self.test_tool(
        "get_infiltration_data",
        f"Get infiltration layer data and soil statistics (BaldEagleCrkMulti2D project: {infiltration_project_path.name})",
        {
            "project_path": str(infiltration_project_path),
            "significant_threshold": 5.0
        }
    )
else:
    # Fallback to current project if infiltration project not available
    await self.test_tool(
        "get_infiltration_data",
        "Get infiltration layer data and soil statistics (fallback - may not have data)",
        {
            "project_path": str(self.project_path),
            "significant_threshold": 5.0
        }
    )
```

### Test Results

The tool consistently returned:

```
HEC-RAS Project: BaldEagleDamBrk
Project Path: C:\GH\ras-commander-mappingbranch\examples\example_projects\BaldEagleCrkMulti2D
HEC-RAS Version: 6.6
================================================================================

No infiltration layer file found in project
```

Even when tested with projects that contained infiltration data files (e.g., BaldEagleCrkMulti2D project which has `Soils Data/Infiltration.hdf`).

### Potential Issues

1. **RASMapper Configuration**: The infiltration layer filename may not be properly stored in the `InfiltrationLayerFilename` column of the RASMapper DataFrame
2. **File Path Resolution**: The path construction logic may not correctly resolve relative paths from the RASMapper configuration
3. **ras-commander Library**: The underlying library may have issues with parsing infiltration layer references from RASMapper files

### Future Work

To restore this tool, investigation would be needed into:
1. How infiltration layer filenames are actually stored in RASMapper files
2. Alternative methods to locate infiltration data files
3. Updates to the ras-commander library to better handle infiltration data references
==================================================

File: c:\GH\ras-commander-mcp-main\package.json
==================================================
{
  "name": "hecras-mcp-server",
  "version": "0.1.0",
  "description": "MCP server for querying HEC-RAS project information",
  "main": "server.py",
  "scripts": {
    "start": "python server.py"
  },
  "mcp": {
    "server": {
      "command": "python",
      "args": ["server.py"]
    }
  },
  "dependencies": {
    "mcp": "*",
    "ras-commander": "*",
    "pandas": "*"
  }
}
==================================================

File: c:\GH\ras-commander-mcp-main\pyproject.toml
==================================================
[project]
name = "ras-commander-mcp"
version = "0.1.0"
description = "MCP server for HEC-RAS project interaction via ras-commander"
readme = "README.md"
license = {text = "MIT"}
authors = [{name = "CLB Engineering Corporation", email = "info@clbengineering.com"}]
requires-python = ">=3.10"
dependencies = [
    "mcp>=1.0.0",
    "ras-commander>=0.1.0",
    "pandas>=1.3.0",
    "h5py>=3.0.0"
]

[project.urls]
Homepage = "https://github.com/gpt-cmdr/ras-commander-mcp"
Documentation = "https://github.com/gpt-cmdr/ras-commander"
Repository = "https://github.com/gpt-cmdr/ras-commander-mcp.git"
"Company" = "https://clbengineering.com/"

[project.scripts]
ras-commander-mcp = "server:run"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = [
    "pytest>=7.0.0",
    "pytest-asyncio>=0.20.0"
]

[tool.hatch.build.targets.wheel]
packages = ["."]
==================================================

File: c:\GH\ras-commander-mcp-main\README.md
==================================================
# HEC-RAS MCP Server

<div align="center">
  <img src="ras_commander_mcp_logo.svg" alt="RAS Commander MCP Logo" width="70%">
</div>  

The RAS Commander MCP (Model Context Protocol) server provides tools for querying HEC-RAS project information using the ras-commander library. This allows Claude Desktop to interact with HEC-RAS hydraulic modeling projects.

**RAS Commander MCP** is an open-source, LLM-forward H&H automation tool provided under MIT license by [CLB Engineering Corporation](https://clbengineering.com/). This is third-party software and is not made by or endorsed by the U.S. Army Corps of Engineers (USACE) Hydrologic Engineering Center (HEC).

For a demonstration of CLB's H&H automation services, contact us at info@clbengineering.com

## Features

- Query comprehensive HEC-RAS project information (plans, geometries, flows, boundaries)
- Extract detailed plan results including unsteady simulation info and runtime metrics
- Explore HDF file structures and extract computation messages
- Support for multiple HEC-RAS versions (6.5, 6.6, etc.)
- Formatted text output suitable for LLM interaction
- Error handling with helpful diagnostics

## Prerequisites

1. **HEC-RAS Installation**: HEC-RAS must be installed on your system (default expects version 6.6)
2. **Python**: Python 3.10+
3. **Claude Desktop**: For MCP integration
4. **uv**: Python package manager (recommended)

## Installation

### Using uv (Recommended)

1. Install uv if you haven't already:
```bash
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
```

2. Clone this repository:
```bash
git clone <repository-url>
cd ras-commander-mcp
```

3. The dependencies will be automatically installed when you run the server with uvx (see Configuration below).

### Using pip (Alternative)

If you prefer to use pip:
```bash
pip install mcp ras-commander pandas
```

## Configuration

### Claude Desktop Integration with uvx (Recommended)

Add the following to your Claude Desktop configuration file (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "hecras": {
      "command": "uvx",
      "args": [
        "--from", "ras-commander-mcp@git+https://github.com/gpt-cmdr/ras-commander-mcp.git",
        "ras-commander-mcp"
      ],
      "env": {
        "HECRAS_VERSION": "6.6"
      }
    }
  }
}
```

### Alternative: Using Local Installation

If you've cloned the repository locally and want to run from source:

```json
{
  "mcpServers": {
    "hecras": {
      "command": "uv",
      "args": ["run", "python", "path/to/your/ras-commander-mcp/server.py"],
      "env": {
        "HECRAS_VERSION": "6.6"
      }
    }
  }
}
```

### HEC-RAS Version Configuration

The MCP server uses HEC-RAS version 6.6 by default. To use a different version:

1. **Set HEC-RAS Version** (if you have a different version installed):
   ```json
   {
     "mcpServers": {
       "hecras": {
         "command": "uvx",
         "args": [
           "--from", "ras-commander-mcp@git+https://github.com/gpt-cmdr/ras-commander-mcp.git",
           "ras-commander-mcp"
         ],
         "env": {
           "HECRAS_VERSION": "6.5"
         }
       }
     }
   }
   ```

2. **Set HEC-RAS Path** (if HEC-RAS is installed in a non-standard location):
   ```json
   {
     "mcpServers": {
       "hecras": {
         "command": "uvx",
         "args": [
           "--from", "ras-commander-mcp@git+https://github.com/gpt-cmdr/ras-commander-mcp.git",
           "ras-commander-mcp"
         ],
         "env": {
           "HECRAS_PATH": "C:\\Program Files\\HEC\\HEC-RAS\\6.5\\HEC-RAS.exe"
         }
       }
     }
   }
   ```

## Usage

### Available Tools

All tools provided by this MCP server leverage the [ras-commander](https://github.com/gpt-cmdr/ras-commander) Python library for advanced HEC-RAS automation capabilities.

1. **hecras_project_summary**: Get comprehensive or selective project information
   - Parameters:
     - `project_path` (required): Full path to HEC-RAS project folder
     - `show_rasprj` (optional): Show project file contents (default: true)
     - `show_plan_df` (optional): Show plan files and metadata (default: true)
     - `show_geom_df` (optional): Show geometry files (default: true)
     - `show_flow_df` (optional): Show steady flow data (default: true)
     - `show_unsteady_df` (optional): Show unsteady flow data (default: true)
     - `show_boundaries` (optional): Show boundary conditions (default: true)
     - `show_rasmap` (optional): Show RASMapper configuration (default: false)
     - `showmore` (optional): Show all columns/verbose mode (default: false)

2. **read_plan_description**: Read multi-line description from a plan file
   - Parameters:
     - `project_path` (required): Full path to HEC-RAS project folder
     - `plan_number` (required): Plan number (e.g., '1', '01', '02')

3. **get_plan_results_summary**: Get comprehensive results from a specific plan
   - Parameters:
     - `project_path` (required): Full path to HEC-RAS project folder
     - `plan_number` (required): Plan number or full path to plan HDF file

4. **get_compute_messages**: Get computation messages and performance metrics
   - Parameters:
     - `project_path` (required): Full path to HEC-RAS project folder
     - `plan_number` (required): Plan number or full path to plan HDF file

5. **get_hdf_structure**: Explore HDF file structure
   - Parameters:
     - `hdf_path` (required): Full path to the HDF file
     - `group_path` (optional): Internal HDF path to start exploration from (default: "/")
     - `paths_only` (optional): Show only paths without details (default: false)

6. **get_projection_info**: Get spatial projection information (WKT)
   - Parameters:
     - `hdf_path` (required): Full path to the HDF file

### Example Usage in Claude

Once configured, you can ask Claude:

- "Query the HEC-RAS project at C:/Projects/MyRiverModel"
- "Show me the plans in the Muncie test project"
- "Get the results summary for plan '01' in my project"
- "Show me the compute messages for plan '1'"
- "Explore the HDF structure of my results file"
- "Get the projection info from my terrain HDF"

## Python Library Reference

This MCP server is built on top of the [ras-commander](https://github.com/gpt-cmdr/ras-commander) Python library, which provides comprehensive programmatic access to HEC-RAS projects. For advanced Python scripting and automation beyond what's available through the MCP interface, refer to the ras-commander documentation.

## Testing

### Using uv

Run the test suite from the project directory:

```bash
# Run all tests
uv run python tests/test_all_tools.py

# Test single tool
uv run python tests/test_single_tool.py

# Test server functionality
uv run python tests/test_server.py
```

### Test Data

The `testdata/` folder contains complete HEC-RAS projects for testing:
- `Muncie/`: Complete project with terrain, results, and boundary conditions
- `BeaverLake/`: Additional test project

## Troubleshooting

1. **ImportError for ras-commander**: Ensure HEC-RAS is properly installed
2. **Project not found**: Verify the project path exists and contains .prj files
3. **Version errors**: Check that the specified HEC-RAS version matches your installation
4. **MCP connection issues**: Verify Claude Desktop configuration and restart Claude

## Development

To modify or extend the server:

1. Clone the repository
2. Make changes to `server.py`
3. Test with: `uv run python tests/test_all_tools.py`
4. Update Claude Desktop configuration if needed

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Trademarks

See [TRADEMARKS.md](TRADEMARKS.md) for trademark information and compliance policies.

## About

**RAS Commander MCP** is developed and maintained by [CLB Engineering Corporation](https://clbengineering.com/) as part of our commitment to advancing H&H automation through open-source tools.

For professional H&H automation services and custom solutions, contact us at info@clbengineering.com
==================================================

File: c:\GH\ras-commander-mcp-main\requirements.txt
==================================================
mcp
ras-commander
pandas
==================================================

File: c:\GH\ras-commander-mcp-main\server.py
==================================================
#!/usr/bin/env python3
"""
HEC-RAS MCP Server

An MCP server that provides tools for querying HEC-RAS project information
using the ras-commander library.
"""

import asyncio
import json
import logging
import os
from pathlib import Path
from typing import Any, Sequence
import pandas as pd
import io

from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# Import ras-commander
try:
    from ras_commander import init_ras_project, HdfBase, HdfResultsPlan, RasPlan
    import h5py
    import numpy as np
except ImportError:
    raise ImportError("ras-commander is not installed. Please install it with: pip install ras-commander")

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Initialize the MCP server
server = Server("hecras-mcp-server")

# Configuration
DEFAULT_RAS_VERSION = "6.6"
# Get HEC-RAS version from environment variable or use default
HECRAS_VERSION = os.environ.get("HECRAS_VERSION", DEFAULT_RAS_VERSION)
# Allow specifying a direct path to HEC-RAS executable
HECRAS_PATH = os.environ.get("HECRAS_PATH", None)

def get_ras_version_info():
    """Get the configured HEC-RAS version or path for display."""
    if HECRAS_PATH:
        return f"HEC-RAS Path: {HECRAS_PATH}"
    else:
        return f"HEC-RAS Version: {HECRAS_VERSION}"

def get_compute_messages_local(hdf_path: Path) -> str:
    """
    Local implementation of get_compute_messages for HEC-RAS plan HDF files.
    
    Extracts computation log messages stored in the HDF file, including timing 
    information, computation tasks, and performance metrics.
    """
    try:
        with h5py.File(hdf_path, 'r') as hdf_file:
            compute_messages_path = '/Results/Summary/Compute Messages (text)'
            
            if compute_messages_path not in hdf_file:
                return "Compute messages not found. The simulation may not have completed or results were not saved properly."
            
            # Extract the compute messages
            compute_messages_dataset = hdf_file[compute_messages_path]
            
            # Handle different data types
            if isinstance(compute_messages_dataset, h5py.Dataset):
                data = compute_messages_dataset[()]
                
                # Convert to string based on data type
                if isinstance(data, bytes):
                    messages_text = data.decode('utf-8')
                elif isinstance(data, np.ndarray):
                    if data.dtype.kind == 'S':  # String array
                        # Join array of strings
                        messages_text = '\n'.join([item.decode('utf-8') if isinstance(item, bytes) else str(item) 
                                                  for item in data])
                    else:
                        messages_text = str(data)
                else:
                    messages_text = str(data)
            else:
                return f"Unexpected data type for compute messages: {type(compute_messages_dataset)}"
            
            # Format the compute messages (stops at "Computation Tasks:")
            formatted_output = format_compute_messages_local(messages_text, str(hdf_path))
            
            # Check token count and truncate if necessary
            # Rough approximation: 1 token ≈ 4 characters
            max_chars = 10000 * 4  # 40,000 characters for 10k tokens
            
            if len(formatted_output) > max_chars:
                # Truncate but preserve last 50 lines
                lines = formatted_output.split('\n')
                last_50_lines = lines[-50:] if len(lines) > 50 else lines
                
                # Find how many characters we can include from the beginning
                last_50_text = '\n'.join(last_50_lines)
                truncation_notice = "\n\n[OUTPUT TRUNCATED: Response exceeded 10,000 tokens. Showing beginning and last 50 lines.]\n\n"
                
                available_chars = max_chars - len(last_50_text) - len(truncation_notice)
                truncated_beginning = formatted_output[:available_chars]
                
                # Find last complete line in truncated beginning
                last_newline = truncated_beginning.rfind('\n')
                if last_newline > 0:
                    truncated_beginning = truncated_beginning[:last_newline]
                
                formatted_output = truncated_beginning + truncation_notice + last_50_text
            
            return formatted_output
            
    except FileNotFoundError:
        return f"HDF file not found: {hdf_path}"
    except Exception as e:
        logger.error(f"Error reading compute messages: {str(e)}")
        return f"Error reading compute messages: {str(e)}"

def format_compute_messages_local(messages_text: str, hdf_file_path: str) -> str:
    """
    Format compute messages for better readability.
    Stops processing when reaching "Computation Tasks:" section.
    """
    lines = messages_text.split('\r\n') if '\r\n' in messages_text else messages_text.split('\n')
    
    formatted_parts = [
        f"Compute Messages from: {Path(hdf_file_path).name}",
        "=" * 80,
        ""
    ]
    
    # Only collect general messages, skip computation tasks and speeds
    general_messages = []
    
    for line in lines:
        line = line.strip()
        if not line:
            continue
            
        # Stop processing when we hit computation tasks or speeds
        if 'Computation Task' in line and '\t' in line:
            break
        elif 'Computation Speed' in line and '\t' in line:
            break
        else:
            general_messages.append(line)
    
    # Add general messages only
    if general_messages:
        formatted_parts.append("General Messages:")
        formatted_parts.append("-" * 40)
        for msg in general_messages:
            if ':' in msg and not msg.startswith('http'):
                key, value = msg.split(':', 1)
                formatted_parts.append(f"{key.strip():40} : {value.strip()}")
            else:
                formatted_parts.append(msg)
    
    return '\n'.join(formatted_parts)

def truncate_output(text: str, max_tokens: int = 10000) -> str:
    """Truncate output to maximum tokens and add notice if truncated."""
    # Rough approximation: 1 token = 4 characters
    max_chars = max_tokens * 4
    
    if len(text) <= max_chars:
        return text
    
    truncated_text = text[:max_chars]
    # Try to truncate at a word boundary
    last_space = truncated_text.rfind(' ')
    if last_space > max_chars * 0.9:  # Only if we don't lose too much
        truncated_text = truncated_text[:last_space]
    
    return truncated_text + "\n\n[OUTPUT TRUNCATED: Response exceeded 10,000 tokens. Please use a more specific query for complete results.]"

def filter_dataframe_columns(df: pd.DataFrame, df_type: str, showmore: bool = False) -> tuple[pd.DataFrame, int]:
    """Filter DataFrame columns for default vs verbose output.
    
    Returns:
        tuple: (filtered_dataframe, omitted_columns_count)
    """
    if df is None or df.empty or showmore:
        return df, 0
    
    # Define columns to omit for each dataframe type
    omit_columns = {
        'plan_df': {
            'Geom File', 'Flow File', 'full_path', 'Geom Path', 'Flow Path',
            'Computation Interval', 'Output Interval', 'Instantaneous Interval',
            'Mapping Interval', 'Detailed Interval', 'HDF Compression', 
            'Computation Threads', 'Tolerated Iterations', 'WS Tolerance',
            'Flow Tolerance', 'Computation Mode', 'Mixed Flow', 'Computation Level',
            'Run WQNet'
        },
        'geom_df': {
            'full_path', 'hdf_path'
        },
        'flow_df': {
            'full_path', 'Number of Profiles', 'River Stations'
        },
        'unsteady_df': {
            'full_path', 'Initial Conditions', 'Flow Multiplier', 
            'Base Flow', 'Wave Celerity'
        },
        'boundaries_df': {
            'full_path', 'hydrograph_data', 'hydrograph_data_path',
            'hydrograph_units', 'hydrograph_description', 'bc_data_path',
            'hydrograph_values', 'Is Critical Boundary', 'Critical Boundary Flow',
            'geometry_number'
        }
    }
    
    columns_to_drop = omit_columns.get(df_type, set())
    
    # Only drop columns that actually exist in the dataframe
    existing_columns_to_drop = [col for col in columns_to_drop if col in df.columns]
    
    if existing_columns_to_drop:
        filtered_df = df.drop(columns=existing_columns_to_drop)
        return filtered_df, len(existing_columns_to_drop)
    else:
        return df, 0

def dataframe_to_text(df: pd.DataFrame, name: str, df_type: str = None, showmore: bool = False) -> str:
    """Convert a pandas DataFrame to a formatted text string with optional column filtering."""
    if df is None or df.empty:
        return f"\n{name}: No data available\n"
    
    # Filter columns if df_type is provided
    display_df = df
    omitted_count = 0
    if df_type and not showmore:
        display_df, omitted_count = filter_dataframe_columns(df, df_type, showmore)
    
    # Use StringIO to capture the DataFrame string representation
    buffer = io.StringIO()
    display_df.to_string(buf=buffer, max_rows=100, max_cols=None)
    
    result = f"\n{name}:"
    if omitted_count > 0:
        result += f" ({omitted_count} columns omitted - use showmore=True to see all)"
    result += f"\n{buffer.getvalue()}\n"
    
    return result

@server.list_tools()
async def handle_list_tools() -> list[Tool]:
    """
    List available tools for HEC-RAS project interaction.
    
    RAS Commander MCP is an open-source, LLM-forward H&H automation tool 
    provided under MIT license by CLB Engineering Corporation (https://clbengineering.com/).
    This is third-party software and is not made by or endorsed by USACE HEC.
    
    For advanced Python automation capabilities, see: https://github.com/gpt-cmdr/ras-commander
    For H&H automation services demonstration, contact: info@clbengineering.com
    """
    return [
        Tool(
            name="hecras_project_summary",
            description="Get comprehensive or selective HEC-RAS project information. Built on the ras-commander library (https://github.com/gpt-cmdr/ras-commander) for advanced HEC-RAS automation.",
            inputSchema={
                "type": "object",
                "properties": {
                    "project_path": {
                        "type": "string",
                        "description": "Full path to the HEC-RAS project folder"
                    },
                    "show_rasprj": {
                        "type": "boolean",
                        "description": "Show project file contents",
                        "default": True
                    },
                    "show_plan_df": {
                        "type": "boolean",
                        "description": "Show plan files and metadata",
                        "default": True
                    },
                    "show_geom_df": {
                        "type": "boolean",
                        "description": "Show geometry files",
                        "default": True
                    },
                    "show_flow_df": {
                        "type": "boolean",
                        "description": "Show steady flow data",
                        "default": True
                    },
                    "show_unsteady_df": {
                        "type": "boolean",
                        "description": "Show unsteady flow data",
                        "default": True
                    },
                    "show_boundaries": {
                        "type": "boolean",
                        "description": "Show boundary conditions",
                        "default": True
                    },
                    "show_rasmap": {
                        "type": "boolean",
                        "description": "Show RASMapper configuration",
                        "default": False
                    },
                    "showmore": {
                        "type": "boolean",
                        "description": "Show all columns (verbose mode)",
                        "default": False
                    }
                },
                "required": ["project_path"]
            }
        ),
        Tool(
            name="read_plan_description",
            description="Read the multi-line description block from a HEC-RAS plan file. Part of the RAS Commander MCP suite by CLB Engineering Corporation.",
            inputSchema={
                "type": "object",
                "properties": {
                    "project_path": {
                        "type": "string",
                        "description": "Full path to the HEC-RAS project folder"
                    },
                    "plan_number": {
                        "type": "string",
                        "description": "Plan number (e.g., '1', '01', '02') or full path to plan file"
                    }
                },
                "required": ["project_path", "plan_number"]
            }
        ),
        Tool(
            name="get_plan_results_summary",
            description="Get comprehensive results summary from a HEC-RAS plan including unsteady info, volume accounting, and runtime data. Powered by ras-commander library.",
            inputSchema={
                "type": "object",
                "properties": {
                    "project_path": {
                        "type": "string",
                        "description": "Full path to the HEC-RAS project folder"
                    },
                    "plan_number": {
                        "type": "string",
                        "description": "Plan number (e.g., '1', '01', '02') or full path to plan HDF file. Single digits will be zero-padded."
                    }
                },
                "required": ["project_path", "plan_number"]
            }
        ),
        Tool(
            name="get_hdf_structure",
            description="Explore the structure of a HEC-RAS HDF file. CAUTION: Use on 3rd level data structures or deeper to avoid output truncation. Part of RAS Commander MCP by CLB Engineering.",
            inputSchema={
                "type": "object",
                "properties": {
                    "hdf_path": {
                        "type": "string",
                        "description": "Full path to the HDF file"
                    },
                    "group_path": {
                        "type": "string",
                        "description": "Internal HDF path to start exploration from",
                        "default": "/"
                    },
                    "paths_only": {
                        "type": "boolean",
                        "description": "If true, only show group paths without datasets/attributes/details. More appropriate for exploratory queries to understand file structure.",
                        "default": False
                    }
                },
                "required": ["hdf_path"]
            }
        ),
        Tool(
            name="get_projection_info",
            description="Get spatial projection information (WKT string) from a HEC-RAS HDF file. Built with ras-commander for advanced geospatial capabilities.",
            inputSchema={
                "type": "object",
                "properties": {
                    "hdf_path": {
                        "type": "string",
                        "description": "Full path to the HDF file"
                    }
                },
                "required": ["hdf_path"]
            }
        ),
        Tool(
            name="get_compute_messages",
            description="Get computation messages and performance metrics from a HEC-RAS plan. RAS Commander MCP - professional H&H automation by CLB Engineering Corporation.",
            inputSchema={
                "type": "object",
                "properties": {
                    "project_path": {
                        "type": "string",
                        "description": "Full path to the HEC-RAS project folder"
                    },
                    "plan_number": {
                        "type": "string",
                        "description": "Plan number (e.g., '1', '01', '02') or full path to plan HDF file. Single digits will be zero-padded."
                    }
                },
                "required": ["project_path", "plan_number"]
            }
        )
    ]

@server.call_tool()
async def handle_call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
    """Handle tool calls."""
    
    if name == "hecras_project_summary":
        try:
            project_path = Path(arguments["project_path"])
            show_rasprj = arguments.get("show_rasprj", True)
            show_plan_df = arguments.get("show_plan_df", True)
            show_geom_df = arguments.get("show_geom_df", True)
            show_flow_df = arguments.get("show_flow_df", True)
            show_unsteady_df = arguments.get("show_unsteady_df", True)
            show_boundaries = arguments.get("show_boundaries", True)
            show_rasmap = arguments.get("show_rasmap", False)
            showmore = arguments.get("showmore", False)
            
            # Use configured version or path
            ras_version = HECRAS_PATH if HECRAS_PATH else HECRAS_VERSION
            
            # Validate project path
            if not project_path.exists() or not project_path.is_dir():
                return [TextContent(
                    type="text",
                    text=f"Error: The specified project folder does not exist or is not a directory: {project_path}"
                )]
            
            # Initialize the RAS project
            logger.info(f"Initializing HEC-RAS project at: {project_path}")
            ras = init_ras_project(project_path, ras_version)
            
            # Build the response
            response_parts = [
                f"HEC-RAS Project: {ras.project_name}",
                f"Project Path: {project_path}",
                get_ras_version_info(),
                "=" * 80
            ]
            
            # Add project file contents if requested
            if show_rasprj and hasattr(ras, 'prj_file') and ras.prj_file:
                try:
                    with open(ras.prj_file, 'r', encoding='utf-8') as f:
                        prj_content = f.read()
                    
                    # Filter out lines that start with specific patterns
                    excluded_prefixes = [
                        'Geom File=', 'Flow File=', 'Unsteady File=', 'Plan File=',
                        'DSS Export Filename=', 'DSS Export Rating Curves=', 'DSS Export Rating Curve Sorted=',
                        'DSS Export Volume Flow Curves=', 'DXF Filename=', 'DXF OffsetX=', 'DXF OffsetY=',
                        'DXF ScaleX=', 'DXF ScaleY=', 'GIS Export Profiles='
                    ]
                    filtered_lines = []
                    
                    for line in prj_content.splitlines():
                        if not any(line.strip().startswith(prefix) for prefix in excluded_prefixes):
                            filtered_lines.append(line)
                    
                    response_parts.append("\nPROJECT FILE CONTENTS (.prj):")
                    response_parts.append("-" * 40)
                    response_parts.append('\n'.join(filtered_lines))
                    response_parts.append("=" * 80)
                except Exception as e:
                    logger.error(f"Error reading project file: {str(e)}")
                    response_parts.append("\nPROJECT FILE: Error reading file")
                    response_parts.append("=" * 80)
            
            # Add plan information
            if show_plan_df and hasattr(ras, 'plan_df') and ras.plan_df is not None:
                response_parts.append(dataframe_to_text(ras.plan_df, "PLANS", "plan_df", showmore))
            
            # Add geometry information
            if show_geom_df and hasattr(ras, 'geom_df') and ras.geom_df is not None:
                response_parts.append(dataframe_to_text(ras.geom_df, "GEOMETRIES", "geom_df", showmore))
            
            # Add flow information
            if show_flow_df and hasattr(ras, 'flow_df') and ras.flow_df is not None:
                response_parts.append(dataframe_to_text(ras.flow_df, "STEADY FLOWS", "flow_df", showmore))
                
            if show_unsteady_df and hasattr(ras, 'unsteady_df') and ras.unsteady_df is not None:
                response_parts.append(dataframe_to_text(ras.unsteady_df, "UNSTEADY FLOWS", "unsteady_df", showmore))
            
            # Add boundary conditions if requested
            if show_boundaries and hasattr(ras, 'boundaries_df') and ras.boundaries_df is not None:
                response_parts.append(dataframe_to_text(ras.boundaries_df, "BOUNDARY CONDITIONS", "boundaries_df", showmore))
            
            # Add RASMapper information if requested
            if show_rasmap and hasattr(ras, 'rasmap_df') and ras.rasmap_df is not None:
                response_parts.append(dataframe_to_text(ras.rasmap_df, "RASMAP CONFIGURATION", "rasmap_df", showmore))
            
            return [TextContent(
                type="text",
                text=truncate_output("\n".join(response_parts))
            )]
            
        except Exception as e:
            logger.error(f"Error querying HEC-RAS project: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error querying HEC-RAS project: {str(e)}\n\nPlease ensure:\n1. The project path is correct\n2. HEC-RAS version is installed at the expected location\n3. The project files are valid"
            )]
    
    
    elif name == "read_plan_description":
        try:
            project_path = Path(arguments["project_path"])
            plan_number = arguments["plan_number"]
            
            # Handle single-digit plan numbers
            if plan_number.isdigit() and len(plan_number) == 1:
                plan_number = plan_number.zfill(2)
            
            if not project_path.exists() or not project_path.is_dir():
                return [TextContent(
                    type="text",
                    text=f"Error: The specified project folder does not exist: {project_path}"
                )]
            
            # Initialize project to get plan info
            ras_version = HECRAS_PATH if HECRAS_PATH else HECRAS_VERSION
            ras = init_ras_project(project_path, ras_version)
            
            # Read the plan description
            description = RasPlan.read_plan_description(plan_number, ras)
            
            response_parts = [
                f"Plan Description for Plan {plan_number}",
                f"Project: {ras.project_name}",
                "=" * 80,
                "",
                description if description else "[No description found]",
                ""
            ]
            
            return [TextContent(
                type="text",
                text="\n".join(response_parts)
            )]
            
        except ValueError as e:
            logger.error(f"Error reading plan description: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error: Plan '{plan_number}' not found in project"
            )]
        except Exception as e:
            logger.error(f"Error reading plan description: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error reading plan description: {str(e)}"
            )]
    
    elif name == "get_plan_results_summary":
        try:
            project_path = Path(arguments["project_path"])
            plan_number = arguments["plan_number"]
            
            # Handle single-digit plan numbers
            if plan_number.isdigit() and len(plan_number) == 1:
                plan_number = plan_number.zfill(2)
            
            if not project_path.exists() or not project_path.is_dir():
                return [TextContent(
                    type="text",
                    text=f"Error: The specified project folder does not exist: {project_path}"
                )]
            
            # Initialize project to get plan info
            ras_version = HECRAS_PATH if HECRAS_PATH else HECRAS_VERSION
            ras = init_ras_project(project_path, ras_version)
            
            # Handle plan identification like ras-commander: plan number ("01", "02") or full HDF path
            plan_hdf_path = None
            
            # Check if plan_number is a full path to an HDF file
            if plan_number.endswith('.hdf') and Path(plan_number).exists():
                plan_hdf_path = Path(plan_number)
            else:
                # Assume it's a plan number (01, 02, etc.) - construct HDF path
                if hasattr(ras, 'plan_df') and ras.plan_df is not None:
                    # Look for plan by plan_number
                    plan_row = ras.plan_df[ras.plan_df['plan_number'] == plan_number]
                    
                    if not plan_row.empty and 'HDF_Results_Path' in plan_row.columns:
                        hdf_rel_path = plan_row['HDF_Results_Path'].iloc[0]
                        if hdf_rel_path:
                            plan_hdf_path = project_path / hdf_rel_path
                
                # If still not found, try direct file construction (common pattern)
                if not plan_hdf_path:
                    # Try common HEC-RAS naming patterns
                    project_name = project_path.name
                    potential_paths = [
                        project_path / f"{project_name}.p{plan_number}.hdf",
                        project_path / f"*.p{plan_number}.hdf"
                    ]
                    
                    for pattern in potential_paths:
                        if '*' in str(pattern):
                            matches = list(project_path.glob(pattern.name))
                            if matches:
                                plan_hdf_path = matches[0]
                                break
                        elif pattern.exists():
                            plan_hdf_path = pattern
                            break
            
            if not plan_hdf_path or not plan_hdf_path.exists():
                return [TextContent(
                    type="text",
                    text=f"Error: Plan '{plan_number}' not found or has no results HDF file"
                )]
            
            response_parts = [
                f"Plan Results Summary: {plan_number}",
                f"HDF Path: {plan_hdf_path}",
                "=" * 80
            ]
            
            # Get unsteady info
            try:
                unsteady_info = HdfResultsPlan.get_unsteady_info(plan_hdf_path)
                response_parts.append(dataframe_to_text(unsteady_info, "UNSTEADY INFO"))
            except Exception as e:
                response_parts.append(f"\nUnsteady info not available: {str(e)}")
            
            # Get unsteady summary
            try:
                unsteady_summary = HdfResultsPlan.get_unsteady_summary(plan_hdf_path)
                response_parts.append(dataframe_to_text(unsteady_summary, "UNSTEADY SUMMARY"))
            except Exception as e:
                response_parts.append(f"\nUnsteady summary not available: {str(e)}")
            
            # Get volume accounting
            try:
                volume_accounting = HdfResultsPlan.get_volume_accounting(plan_hdf_path)
                if volume_accounting is not None:
                    response_parts.append(dataframe_to_text(volume_accounting, "VOLUME ACCOUNTING"))
                else:
                    response_parts.append("\nVolume accounting not available")
            except Exception as e:
                response_parts.append(f"\nVolume accounting error: {str(e)}")
            
            # Get runtime data
            try:
                runtime_data = HdfResultsPlan.get_runtime_data(plan_hdf_path)
                if runtime_data is not None:
                    response_parts.append(dataframe_to_text(runtime_data, "RUNTIME DATA"))
                else:
                    response_parts.append("\nRuntime data not available")
            except Exception as e:
                response_parts.append(f"\nRuntime data error: {str(e)}")
            
            return [TextContent(
                type="text",
                text=truncate_output("\n".join(response_parts))
            )]
            
        except Exception as e:
            logger.error(f"Error getting plan results summary: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error getting plan results summary: {str(e)}"
            )]
    
    elif name == "get_compute_messages":
        try:
            project_path = Path(arguments["project_path"])
            plan_number = arguments["plan_number"]
            
            # Handle single-digit plan numbers
            if plan_number.isdigit() and len(plan_number) == 1:
                plan_number = plan_number.zfill(2)
            
            if not project_path.exists() or not project_path.is_dir():
                return [TextContent(
                    type="text",
                    text=f"Error: The specified project folder does not exist: {project_path}"
                )]
            
            # Initialize project to get plan info
            ras_version = HECRAS_PATH if HECRAS_PATH else HECRAS_VERSION
            ras = init_ras_project(project_path, ras_version)
            
            # Handle plan identification (reuse logic from get_plan_results_summary)
            plan_hdf_path = None
            
            # Check if plan_number is a full path to an HDF file
            if plan_number.endswith('.hdf') and Path(plan_number).exists():
                plan_hdf_path = Path(plan_number)
            else:
                # Assume it's a plan number - construct HDF path
                if hasattr(ras, 'plan_df') and ras.plan_df is not None:
                    # Look for plan by plan_number
                    plan_row = ras.plan_df[ras.plan_df['plan_number'] == plan_number]
                    
                    if not plan_row.empty and 'HDF_Results_Path' in plan_row.columns:
                        hdf_rel_path = plan_row['HDF_Results_Path'].iloc[0]
                        if hdf_rel_path:
                            plan_hdf_path = project_path / hdf_rel_path
                
                # If still not found, try direct file construction
                if not plan_hdf_path:
                    project_name = project_path.name
                    potential_paths = [
                        project_path / f"{project_name}.p{plan_number}.hdf",
                        project_path / f"*.p{plan_number}.hdf"
                    ]
                    
                    for pattern in potential_paths:
                        if '*' in str(pattern):
                            matches = list(project_path.glob(pattern.name))
                            if matches:
                                plan_hdf_path = matches[0]
                                break
                        elif pattern.exists():
                            plan_hdf_path = pattern
                            break
            
            if not plan_hdf_path or not plan_hdf_path.exists():
                return [TextContent(
                    type="text",
                    text=f"Error: Plan '{plan_number}' not found or has no results HDF file"
                )]
            
            # Get compute messages using local implementation
            compute_messages = get_compute_messages_local(plan_hdf_path)
            
            return [TextContent(
                type="text",
                text=compute_messages
            )]
            
        except Exception as e:
            logger.error(f"Error getting compute messages: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error getting compute messages: {str(e)}"
            )]
    
    elif name == "get_hdf_structure":
        try:
            hdf_path = Path(arguments["hdf_path"])
            group_path = arguments.get("group_path", "/")
            paths_only = arguments.get("paths_only", False)
            
            if not hdf_path.exists():
                return [TextContent(
                    type="text",
                    text=f"Error: The specified HDF file does not exist: {hdf_path}"
                )]
            
            if paths_only:
                # Custom implementation for paths-only exploration
                import h5py
                paths = []
                
                def collect_paths(name, obj):
                    if isinstance(obj, h5py.Group):
                        paths.append(f"Group: /{name}")
                    elif isinstance(obj, h5py.Dataset):
                        paths.append(f"Dataset: /{name}")
                
                with h5py.File(hdf_path, 'r') as f:
                    if group_path != "/":
                        if group_path in f:
                            f[group_path].visititems(collect_paths)
                        else:
                            return [TextContent(
                                type="text",
                                text=f"Error: Group path '{group_path}' not found in HDF file"
                            )]
                    else:
                        f.visititems(collect_paths)
                
                response_parts = [
                    f"HDF File Structure (Paths Only): {hdf_path}",
                    f"Starting from: {group_path}",
                    "=" * 80,
                    "\n".join(sorted(paths))
                ]
            else:
                # Capture the full structure output
                import io
                import sys
                old_stdout = sys.stdout
                sys.stdout = buffer = io.StringIO()
                
                try:
                    HdfBase.get_dataset_info(hdf_path, group_path)
                    structure_output = buffer.getvalue()
                finally:
                    sys.stdout = old_stdout
                
                response_parts = [
                    f"HDF File Structure: {hdf_path}",
                    f"Starting from: {group_path}",
                    "=" * 80,
                    structure_output
                ]
            
            return [TextContent(
                type="text",
                text=truncate_output("\n".join(response_parts))
            )]
            
        except Exception as e:
            logger.error(f"Error getting HDF structure: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error getting HDF structure: {str(e)}"
            )]
    
    elif name == "get_projection_info":
        try:
            hdf_path = Path(arguments["hdf_path"])
            
            if not hdf_path.exists():
                return [TextContent(
                    type="text",
                    text=f"Error: The specified HDF file does not exist: {hdf_path}"
                )]
            
            projection_wkt = HdfBase.get_projection(hdf_path)
            
            response_parts = [
                f"Projection Info for: {hdf_path}",
                "=" * 80
            ]
            
            if projection_wkt:
                response_parts.append(f"\nWKT String:\n{projection_wkt}")
            else:
                response_parts.append("\nNo projection information found")
            
            return [TextContent(
                type="text",
                text=truncate_output("\n".join(response_parts))
            )]
            
        except Exception as e:
            logger.error(f"Error getting projection info: {str(e)}")
            return [TextContent(
                type="text",
                text=f"Error getting projection info: {str(e)}"
            )]
    
    else:
        return [TextContent(
            type="text",
            text=f"Error: Unknown tool: {name}"
        )]

async def main():
    """Main entry point for the MCP server."""
    async with stdio_server() as (read_stream, write_stream):
        logger.info("Starting HEC-RAS MCP Server...")
        logger.info("RAS Commander MCP by CLB Engineering Corporation")
        logger.info("Built on ras-commander: https://github.com/gpt-cmdr/ras-commander")
        
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="hecras-mcp-server",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

def run():
    """Entry point for console script."""
    asyncio.run(main())

if __name__ == "__main__":
    asyncio.run(main())
==================================================

File: c:\GH\ras-commander-mcp-main\TRADEMARKS.md
==================================================
# TRADEMARKS.md

## Project Trademark

"RAS Commander"™ is an unregistered trademark of CLB Engineering Corporation. The mark is used solely to identify this open‑source software project and related materials.

## Third‑Party Trademarks

* **HEC‑RAS**™ is a trademark of the U.S. Army Corps of Engineers (USACE) Hydrologic Engineering Center (HEC).


* This "HEC-RAS MCP" is an independent open source projects and is **not** affiliated with, endorsed by, or sponsored by USACE or HEC.

All other product names, logos, and brands mentioned in this repository are property of their respective owners and are used for identification purposes only.

## Naming & Compliance Policy

We respect the trademark rights and license terms of USACE and all other rights holders. If USACE any other rightful owner—objects to our use of "HEC-RAS" in the project name, we will promptly rename the project and update all references within **30 days** of receiving written notice.

## Contact

To raise any trademark or licensing concerns, please open an issue in this repository.
==================================================

File: c:\GH\ras-commander-mcp-main\__main__.py
==================================================
"""Allow the package to be run as a module."""
import asyncio
from server import main

if __name__ == "__main__":
    asyncio.run(main())
==================================================

File: c:\GH\ras-commander-mcp-main\ras-commander reference files\api.md
==================================================
# RAS Commander API Documentation

This document provides a detailed reference for the public Application Programming Interface (API) of the `ras_commander` library. It lists all public classes and functions available for interacting with HEC-RAS projects.

## Introduction to Decorators

Many functions within the `ras_commander` library utilize decorators to provide common functionality like logging and input standardization. Understanding these decorators is key to using the API effectively.

### `@log_call`

*   **Purpose:** Automatically logs the entry and exit of a function call at the DEBUG level using the library's configured logger.
*   **Usage:** Applied to most public methods in the `Ras*` and `Hdf*` classes.
*   **Benefit:** Reduces boilerplate logging code and provides a consistent way to trace function execution for debugging purposes. You can configure the overall logging level using `logging.getLogger('ras_commander').setLevel(logging.LEVEL)`.

### `@standardize_input(file_type='plan_hdf'|'geom_hdf')`

*   **Purpose:** Standardizes the input for functions that operate on HEC-RAS HDF files (`.hdf`). It ensures that the function receives a validated `pathlib.Path` object pointing to the correct HDF file, regardless of the input format provided by the user.
*   **Usage:** Primarily used by methods within the `Hdf*` classes.
*   **Accepted Inputs:** The decorator can handle various input types for the HDF file path argument (usually the first argument or `hdf_path` keyword):
    *   `str`: A plan/geometry number (e.g., "01"), a plan prefix number (e.g., "p01"), or a full file path.
    *   `int`: A plan/geometry number (e.g., 1).
    *   `pathlib.Path`: A Path object pointing to the HDF file.
    *   `h5py.File`: An opened h5py File object (the decorator extracts the filename).
*   **`file_type` Argument:**
    *   `'plan_hdf'`: When resolving numbers, the decorator looks for the corresponding plan results HDF file (e.g., `ProjectName.p01.hdf`). This is the default.
    *   `'geom_hdf'`: When resolving numbers, the decorator looks for the corresponding geometry HDF file (e.g., `ProjectName.g01.hdf`).
*   **RAS Object Context:** The decorator uses the provided `ras_object` (or the global `ras` instance) to look up file paths when numbers are given as input. Ensure the relevant `RasPrj` object is initialized.
*   **Validation:** The decorator verifies that the resulting path points to an existing file before passing it to the decorated function.

---

## Class: RasPrj

Manages HEC-RAS project data and state. Provides access to project files, plans, geometries, flows, and boundary conditions. Can be used as a global `ras` object or instantiated for multi-project workflows.

### `RasPrj.initialize(project_folder, ras_exe_path, suppress_logging=True)`

*   **Purpose:** Initializes a `RasPrj` instance. **Note:** Users should typically call `init_ras_project()` instead.
*   **Parameters:**
    *   `project_folder` (`str` or `Path`): Path to the HEC-RAS project folder.
    *   `ras_exe_path` (`str` or `Path`): Path to the HEC-RAS executable.
    *   `suppress_logging` (`bool`, optional, default=`True`): Suppresses detailed initialization logs if True.
*   **Returns:** `None`. Modifies the instance in place.
*   **Raises:** `ValueError` if no `.prj` file is found.

### `RasPrj.check_initialized()`

*   **Purpose:** Checks if the `RasPrj` instance has been initialized.
*   **Parameters:** None.
*   **Returns:** `None`.
*   **Raises:** `RuntimeError` if the project is not initialized.

### `RasPrj.find_ras_prj(folder_path)`

*   **Purpose:** (Static method) Finds the main HEC-RAS project file (`.prj`) within a folder using various heuristics.
*   **Parameters:**
    *   `folder_path` (`str` or `Path`): Path to the folder to search.
*   **Returns:** `Path` object for the found `.prj` file, or `None` if not found.

### `RasPrj.get_project_name()`

*   **Purpose:** Gets the name of the initialized HEC-RAS project (filename without extension).
*   **Parameters:** None.
*   **Returns:** (`str`): The project name.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_prj_entries(entry_type)`

*   **Purpose:** Retrieves entries (plans, flows, geoms, unsteady) listed in the project file (`.prj`).
*   **Parameters:**
    *   `entry_type` (`str`): Type of entry ('Plan', 'Flow', 'Unsteady', 'Geom').
*   **Returns:** `pd.DataFrame`: DataFrame containing information about the specified entry type found in the project.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_plan_entries()`

*   **Purpose:** Retrieves all plan file entries (`.p*`) listed in the project file.
*   **Parameters:** None.
*   **Returns:** `pd.DataFrame`: DataFrame of plan entries with details parsed from plan files.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_flow_entries()`

*   **Purpose:** Retrieves all steady flow file entries (`.f*`) listed in the project file.
*   **Parameters:** None.
*   **Returns:** `pd.DataFrame`: DataFrame of steady flow entries.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_unsteady_entries()`

*   **Purpose:** Retrieves all unsteady flow file entries (`.u*`) listed in the project file.
*   **Parameters:** None.
*   **Returns:** `pd.DataFrame`: DataFrame of unsteady flow entries with details parsed from unsteady files.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_geom_entries()`

*   **Purpose:** Retrieves all geometry file entries (`.g*`) listed in the project file.
*   **Parameters:** None.
*   **Returns:** `pd.DataFrame`: DataFrame of geometry entries including paths to associated `.hdf` files.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_hdf_entries()`

*   **Purpose:** Retrieves plan entries that have a corresponding HDF results file (`.p*.hdf`).
*   **Parameters:** None.
*   **Returns:** `pd.DataFrame`: Filtered DataFrame of plan entries with existing HDF results files.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.print_data()`

*   **Purpose:** Prints a summary of the initialized project data (paths, file counts, dataframes) to the log (INFO level).
*   **Parameters:** None.
*   **Returns:** `None`.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj.get_plan_value(plan_number_or_path, key, ras_object=None)`

*   **Purpose:** (Static method, but often called on instance) Retrieves a specific value for a given key from a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number (e.g., "01") or full path to the plan file.
    *   `key` (`str`): The keyword in the plan file (e.g., 'Computation Interval', 'Short Identifier').
    *   `ras_object` (`RasPrj`, optional): Instance to use context from (project path). Defaults to global `ras`.
*   **Returns:** (`Any`): The value associated with the key, or `None` if not found. Type depends on the key (str, int).
*   **Raises:** `ValueError` if plan file not found, `IOError` on read error.

### `RasPrj.get_boundary_conditions()`

*   **Purpose:** Parses all unsteady flow files in the project to extract and structure boundary condition information.
*   **Parameters:** None.
*   **Returns:** `pd.DataFrame`: DataFrame containing detailed boundary condition data (location, type, parameters, associated unsteady file info). Returns empty DataFrame if no unsteady files or boundaries are found.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPrj` Attributes

*   `project_folder` (`Path`): Path to the project folder.
*   `project_name` (`str`): Name of the project.
*   `prj_file` (`Path`): Path to the project file.
*   `ras_exe_path` (`str`): Path to the HEC-RAS executable.
*   `plan_df` (`pd.DataFrame`): DataFrame containing plan file information.
*   `flow_df` (`pd.DataFrame`): DataFrame containing flow file information.
*   `unsteady_df` (`pd.DataFrame`): DataFrame containing unsteady flow file information.
*   `geom_df` (`pd.DataFrame`): DataFrame containing geometry file information.
*   `boundaries_df` (`pd.DataFrame`): DataFrame containing boundary condition information.
*   `rasmap_df` (`pd.DataFrame`): DataFrame containing RASMapper configuration data including paths to terrain, soil layer, infiltration, and land cover data.

---

## Standalone Functions

These functions are available directly under the `ras_commander` import.

### `init_ras_project(ras_project_folder, ras_version=None, ras_object=None)`

*   **Purpose:** Primary function to initialize a `RasPrj` object (either global `ras` or a custom instance) for a specific HEC-RAS project.
*   **Parameters:**
    *   `ras_project_folder` (`str` or `Path`): Path to the HEC-RAS project folder.
    *   `ras_version` (`str`, optional): HEC-RAS version (e.g., "6.6") or full path to `Ras.exe`. Defaults to auto-detection or global setting.
    *   `ras_object` (`RasPrj`, optional): If `None`, initializes the global `ras` object. If a `RasPrj` instance, initializes that instance. If any other value (e.g., a string like "new"), creates and returns a *new* `RasPrj` instance. **Also updates the global `ras` object regardless.**
*   **Returns:** (`RasPrj`): The initialized `RasPrj` instance (either the one passed in, the global `ras`, or a newly created one).
*   **Raises:** `FileNotFoundError` if folder doesn't exist, `ValueError` if `.prj` file not found.

### `get_ras_exe(ras_version=None)`

*   **Purpose:** Determines the full path to the HEC-RAS executable based on version number or explicit path.
*   **Parameters:**
    *   `ras_version` (`str`, optional): Version string (e.g., "6.5") or full path to `Ras.exe`. If `None`, uses global `ras` object's path or defaults to "Ras.exe".
*   **Returns:** (`str`): Full path to the `Ras.exe` executable. Returns "Ras.exe" if lookup fails.

---

## Class: RasPlan

Contains static methods for operating on HEC-RAS plan files (`.p*`). Assumes a `RasPrj` object (defaulting to global `ras`) is initialized for context.

### `RasPlan.set_geom(plan_number, new_geom, ras_object=None)`

*   **Purpose:** Updates a plan file to use a different geometry file.
*   **Parameters:**
    *   `plan_number` (`str` or `int`): Plan number to modify (e.g., "01", 1).
    *   `new_geom` (`str` or `int`): Geometry number to assign (e.g., "02", 2).
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `pd.DataFrame`: The updated *geometry* DataFrame of the `ras_object`.
*   **Raises:** `ValueError` if `new_geom` not found, `FileNotFoundError`, `IOError`.

### `RasPlan.set_steady(plan_number, new_steady_flow_number, ras_object=None)`

*   **Purpose:** Updates a plan file to use a specific steady flow file.
*   **Parameters:**
    *   `plan_number` (`str`): Plan number (e.g., "01").
    *   `new_steady_flow_number` (`str`): Steady flow number (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and updates the `ras_object`.
*   **Raises:** `ValueError` if `new_steady_flow_number` not found, `FileNotFoundError`, `IOError`.

### `RasPlan.set_unsteady(plan_number, new_unsteady_flow_number, ras_object=None)`

*   **Purpose:** Updates a plan file to use a specific unsteady flow file.
*   **Parameters:**
    *   `plan_number` (`str`): Plan number (e.g., "01").
    *   `new_unsteady_flow_number` (`str`): Unsteady flow number (e.g., "02").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and updates the `ras_object`.
*   **Raises:** `ValueError` if `new_unsteady_flow_number` not found, `FileNotFoundError`, `IOError`.

### `RasPlan.set_num_cores(plan_number_or_path, num_cores, ras_object=None)`

*   **Purpose:** Sets the number of cores (`UNET D1 Cores`, `UNET D2 Cores`, `PS Cores`) in a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `num_cores` (`int`): Number of cores to set (0 for 'All Available').
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and updates the `ras_object`.
*   **Raises:** `FileNotFoundError`, `IOError`.

### `RasPlan.set_geom_preprocessor(file_path, run_htab, use_ib_tables, ras_object=None)`

*   **Purpose:** Modifies the `Run HTab` and `UNET Use Existing IB Tables` settings in a plan file.
*   **Parameters:**
    *   `file_path` (`str` or `Path`): Full path to the plan file.
    *   `run_htab` (`int`): `0` (use existing) or `-1` (force recompute).
    *   `use_ib_tables` (`int`): `0` (use existing) or `-1` (force recompute).
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and updates the `ras_object`.
*   **Raises:** `ValueError` for invalid flag values, `FileNotFoundError`, `IOError`.

### `RasPlan.clone_plan(template_plan, new_plan_shortid=None, ras_object=None)`

*   **Purpose:** Creates a new plan file by copying a template plan, assigns the next available plan number, optionally updates the Short Identifier, and updates the project file.
*   **Parameters:**
    *   `template_plan` (`str`): Plan number to use as template (e.g., "01").
    *   `new_plan_shortid` (`str`, optional): New Short Identifier (max 24 chars). If `None`, appends "_copy".
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The number of the newly created plan (e.g., "03").
*   **Raises:** `FileNotFoundError` if template not found, `IOError`.

### `RasPlan.clone_unsteady(template_unsteady, ras_object=None)`

*   **Purpose:** Creates a new unsteady flow file (`.u*` and associated `.hdf`) by copying a template, assigns the next available number, and updates the project file.
*   **Parameters:**
    *   `template_unsteady` (`str`): Unsteady flow number to use as template (e.g., "02").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The number of the newly created unsteady flow file (e.g., "03").
*   **Raises:** `FileNotFoundError` if template not found, `IOError`.

### `RasPlan.clone_steady(template_flow, ras_object=None)`

*   **Purpose:** Creates a new steady flow file (`.f*`) by copying a template, assigns the next available number, and updates the project file.
*   **Parameters:**
    *   `template_flow` (`str`): Steady flow number to use as template (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The number of the newly created steady flow file (e.g., "02").
*   **Raises:** `FileNotFoundError` if template not found, `IOError`.

### `RasPlan.clone_geom(template_geom, ras_object=None)`

*   **Purpose:** Creates a new geometry file (`.g*` and associated `.hdf`) by copying a template, assigns the next available number, and updates the project file.
*   **Parameters:**
    *   `template_geom` (`str`): Geometry number to use as template (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The number of the newly created geometry file (e.g., "03").
*   **Raises:** `FileNotFoundError` if template not found, `IOError`.

### `RasPlan.get_next_number(existing_numbers)`

*   **Purpose:** (Static utility) Finds the smallest unused positive integer number given a list of existing numbers (as strings), returned as a zero-padded string.
*   **Parameters:**
    *   `existing_numbers` (`list` of `str`): List of existing numbers (e.g., ['01', '03']).
*   **Returns:** (`str`): The next available number (e.g., "02").

### `RasPlan.get_plan_value(plan_number_or_path, key, ras_object=None)`

*   **Purpose:** Retrieves a specific value for a given key from a plan file. (See also `RasPrj.get_plan_value`).
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `key` (`str`): Keyword in the plan file (e.g., 'Computation Interval').
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`Any`): The value associated with the key, or `None` if not found. Type depends on the key.
*   **Raises:** `ValueError` if plan file not found, `IOError`.

### `RasPlan.get_results_path(plan_number, ras_object=None)`

*   **Purpose:** Gets the expected path to the HDF results file (`.p*.hdf`) for a given plan number. Checks if the file exists.
*   **Parameters:**
    *   `plan_number` (`str`): Plan number (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str` or `None`): Full path to the HDF results file if it exists, otherwise `None`.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPlan.get_plan_path(plan_number, ras_object=None)`

*   **Purpose:** Gets the full path to a plan file (`.p*`) given its number.
*   **Parameters:**
    *   `plan_number` (`str`): Plan number (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str` or `None`): Full path to the plan file, or `None` if not found in the project.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPlan.get_flow_path(flow_number, ras_object=None)`

*   **Purpose:** Gets the full path to a steady flow file (`.f*`) given its number.
*   **Parameters:**
    *   `flow_number` (`str`): Steady flow number (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str` or `None`): Full path to the flow file, or `None` if not found.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPlan.get_unsteady_path(unsteady_number, ras_object=None)`

*   **Purpose:** Gets the full path to an unsteady flow file (`.u*`) given its number.
*   **Parameters:**
    *   `unsteady_number` (`str`): Unsteady flow number (e.g., "02").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str` or `None`): Full path to the unsteady file, or `None` if not found.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPlan.get_geom_path(geom_number, ras_object=None)`

*   **Purpose:** Gets the full path to a geometry file (`.g*`) given its number.
*   **Parameters:**
    *   `geom_number` (`str`): Geometry number (e.g., "01").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str` or `None`): Full path to the geometry file, or `None` if not found.
*   **Raises:** `RuntimeError` if not initialized.

### `RasPlan.update_run_flags(plan_number_or_path, geometry_preprocessor=None, unsteady_flow_simulation=None, run_sediment=None, post_processor=None, floodplain_mapping=None, ras_object=None)`

*   **Purpose:** Updates the run flags (e.g., `Run HTab`, `Run UNet`, `Run RASMapper`) in a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `geometry_preprocessor` (`bool`, optional): Set `Run HTab` (True=1, False=0).
    *   `unsteady_flow_simulation` (`bool`, optional): Set `Run UNet` (True=1, False=0).
    *   `run_sediment` (`bool`, optional): Set `Run Sediment` (True=1, False=0).
    *   `post_processor` (`bool`, optional): Set `Run PostProcess` (True=1, False=0).
    *   `floodplain_mapping` (`bool`, optional): Set `Run RASMapper` (True=0, False=-1). **Note inverted logic for RASMapper**.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.update_plan_intervals(plan_number_or_path, computation_interval=None, output_interval=None, instantaneous_interval=None, mapping_interval=None, ras_object=None)`

*   **Purpose:** Updates time intervals (computation, output, mapping, etc.) in a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `computation_interval` (`str`, optional): E.g., "1MIN", "10SEC", "1HOUR".
    *   `output_interval` (`str`, optional): E.g., "1HOUR", "30MIN".
    *   `instantaneous_interval` (`str`, optional): E.g., "1HOUR", "15MIN".
    *   `mapping_interval` (`str`, optional): E.g., "1HOUR", "15MIN".
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file.
*   **Raises:** `ValueError` if plan not found or interval invalid, `IOError`.

### `RasPlan.update_plan_description(plan_number_or_path, description, ras_object=None)`

*   **Purpose:** Updates the multi-line description block within a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `description` (`str`): The new description text (can be multi-line).
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and updates the `ras_object`.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.read_plan_description(plan_number_or_path, ras_object=None)`

*   **Purpose:** Reads the multi-line description block from a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The description text, or "" if not found.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.update_simulation_date(plan_number_or_path, start_date, end_date, ras_object=None)`

*   **Purpose:** Updates the simulation start and end date/time in a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `start_date` (`datetime`): Simulation start datetime object.
    *   `end_date` (`datetime`): Simulation end datetime object.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and updates the `ras_object`.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.get_shortid(plan_number_or_path, ras_object=None)`

*   **Purpose:** Gets the 'Short Identifier' value from a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The Short Identifier, or "" if not found.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.set_shortid(plan_number_or_path, new_shortid, ras_object=None)`

*   **Purpose:** Sets the 'Short Identifier' value in a plan file (max 24 chars).
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `new_shortid` (`str`): New identifier (will be truncated if > 24 chars).
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.get_plan_title(plan_number_or_path, ras_object=None)`

*   **Purpose:** Gets the 'Plan Title' value from a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`str`): The Plan Title, or "" if not found.
*   **Raises:** `ValueError` if plan not found, `IOError`.

### `RasPlan.set_plan_title(plan_number_or_path, new_title, ras_object=None)`

*   **Purpose:** Sets the 'Plan Title' value in a plan file.
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `new_title` (`str`): New title.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file.
*   **Raises:** `ValueError` if plan not found, `IOError`.

---

## Class: RasGeo

Contains static methods for operating on HEC-RAS geometry files (`.g*`) and associated preprocessor files. Assumes a `RasPrj` object (defaulting to global `ras`) is initialized.

### `RasGeo.clear_geompre_files(plan_files=None, ras_object=None)`

*   **Purpose:** Deletes geometry preprocessor files (`.c*`) associated with specified plan files. This forces HEC-RAS to recompute hydraulic tables based on the geometry. **Note:** Does not currently clear IB tables or HDF geometry tables.
*   **Parameters:**
    *   `plan_files` (`str`, `Path`, `List[Union[str, Path]]`, optional): Plan file path(s) or number(s). If `None`, clears for all plans in the project.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Deletes files and updates the `ras_object`'s geometry DataFrame.
*   **Raises:** `PermissionError`, `OSError`.

### `RasGeo.get_mannings_baseoverrides(geom_file_path)`

*   **Purpose:** Reads the base Manning's n table from a HEC-RAS geometry file.
*   **Parameters:**
    *   `geom_file_path` (Input handled by `@standardize_input`): Path identifier for the geometry file (.g##).
*   **Returns:** `pd.DataFrame`: DataFrame with Table Number, Land Cover Name, and Base Manning's n Value.

### `RasGeo.get_mannings_regionoverrides(geom_file_path)`

*   **Purpose:** Reads the Manning's n region overrides from a HEC-RAS geometry file.
*   **Parameters:**
    *   `geom_file_path` (Input handled by `@standardize_input`): Path identifier for the geometry file (.g##).
*   **Returns:** `pd.DataFrame`: DataFrame with Table Number, Land Cover Name, MainChannel value, and Region Name.

### `RasGeo.set_mannings_baseoverrides(geom_file_path, mannings_data)`

*   **Purpose:** Writes base Manning's n values to a HEC-RAS geometry file.
*   **Parameters:**
    *   `geom_file_path` (Input handled by `@standardize_input`): Path identifier for the geometry file (.g##).
    *   `mannings_data` (`pd.DataFrame`): DataFrame with columns 'Table Number', 'Land Cover Name', and 'Base Manning\'s n Value'.
*   **Returns:** `bool`: True if successful.

### `RasGeo.set_mannings_regionoverrides(geom_file_path, mannings_data)`

*   **Purpose:** Writes regional Manning's n overrides to a HEC-RAS geometry file.
*   **Parameters:**
    *   `geom_file_path` (Input handled by `@standardize_input`): Path identifier for the geometry file (.g##).
    *   `mannings_data` (`pd.DataFrame`): DataFrame with columns 'Table Number', 'Land Cover Name', 'MainChannel', and 'Region Name'.
*   **Returns:** `bool`: True if successful.

---

## Class: RasUnsteady

Contains static methods for operating on HEC-RAS unsteady flow files (`.u*`). Assumes a `RasPrj` object (defaulting to global `ras`) is initialized.

### `RasUnsteady.update_flow_title(unsteady_file, new_title, ras_object=None)`

*   **Purpose:** Updates the 'Flow Title' line within an unsteady flow file (max 24 chars).
*   **Parameters:**
    *   `unsteady_file` (`str` or `Path`): Unsteady flow number or full path.
    *   `new_title` (`str`): The new title (will be truncated if > 24 chars).
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the file and updates the `ras_object`.
*   **Raises:** `FileNotFoundError`, `PermissionError`, `IOError`.

### `RasUnsteady.update_restart_settings(unsteady_file, use_restart, restart_filename=None, ras_object=None)`

*   **Purpose:** Enables or disables the use of a restart file (`.rst`) in an unsteady flow file.
*   **Parameters:**
    *   `unsteady_file` (`str` or `Path`): Unsteady flow number or full path.
    *   `use_restart` (`bool`): `True` to enable restart, `False` to disable.
    *   `restart_filename` (`str`, optional): Path to the `.rst` file (required if `use_restart` is `True`).
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the file and updates the `ras_object`.
*   **Raises:** `ValueError` if `restart_filename` missing, `FileNotFoundError`, `PermissionError`, `IOError`.

### `RasUnsteady.extract_boundary_and_tables(unsteady_file, ras_object=None)`

*   **Purpose:** Parses an unsteady flow file to extract boundary condition definitions and associated time-series data tables (e.g., Flow Hydrograph).
*   **Parameters:**
    *   `unsteady_file` (`str` or `Path`): Unsteady flow number or full path.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `pd.DataFrame`: DataFrame where each row represents a boundary condition. Includes columns for location (`River Name`, `Reach Name`, etc.), `DSS File`, and a `Tables` column containing a dictionary of `pd.DataFrame` objects for each time-series table found.
*   **Raises:** `FileNotFoundError`, `PermissionError`.

### `RasUnsteady.print_boundaries_and_tables(boundaries_df)`

*   **Purpose:** Prints the boundary conditions and tables (extracted by `extract_boundary_and_tables`) to the console in a readable format.
*   **Parameters:**
    *   `boundaries_df` (`pd.DataFrame`): DataFrame returned by `extract_boundary_and_tables`.
*   **Returns:** `None`.

### `RasUnsteady.identify_tables(lines)`

*   **Purpose:** (Static utility) Scans lines from an unsteady file and identifies the name and line ranges of data tables.
*   **Parameters:**
    *   `lines` (`List[str]`): List of lines read from the unsteady file.
*   **Returns:** `List[Tuple[str, int, int]]`: List of tuples `(table_name, start_line_index, end_line_index)`.

### `RasUnsteady.parse_fixed_width_table(lines, start, end)`

*   **Purpose:** (Static utility) Parses the fixed-width numeric data within identified table lines.
*   **Parameters:**
    *   `lines` (`List[str]`): List of lines read from the unsteady file.
    *   `start` (`int`): Starting line index (inclusive) of the table data.
    *   `end` (`int`): Ending line index (exclusive) of the table data.
*   **Returns:** `pd.DataFrame`: DataFrame with a single 'Value' column containing the numeric data.

### `RasUnsteady.extract_tables(unsteady_file, ras_object=None)`

*   **Purpose:** Extracts all numeric data tables (like hydrographs, gate openings) from an unsteady file into a dictionary of DataFrames.
*   **Parameters:**
    *   `unsteady_file` (`str` or `Path`): Unsteady flow number or full path.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `Dict[str, pd.DataFrame]`: Dictionary mapping table names (e.g., 'Flow Hydrograph=') to DataFrames containing the table values.
*   **Raises:** `FileNotFoundError`, `PermissionError`.

### `RasUnsteady.write_table_to_file(unsteady_file, table_name, df, start_line, ras_object=None)`

*   **Purpose:** Writes a modified DataFrame back into an unsteady flow file, formatting it correctly into the fixed-width structure required by HEC-RAS.
*   **Parameters:**
    *   `unsteady_file` (`str` or `Path`): Unsteady flow number or full path.
    *   `table_name` (`str`): Name of the table being written (must match the header in the file).
    *   `df` (`pd.DataFrame`): DataFrame containing the new values (must have a 'Value' column).
    *   `start_line` (`int`): Line index where the original table data started.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the file.
*   **Raises:** `FileNotFoundError`, `PermissionError`, `IOError`.

---

## Class: RasUtils

Contains general static utility functions used across the `ras_commander` library.

### `RasUtils.create_directory(directory_path, ras_object=None)`

*   **Purpose:** Ensures a directory exists, creating it (and any parent directories) if necessary.
*   **Parameters:**
    *   `directory_path` (`Path`): The directory path to ensure.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`Path`): The ensured directory path.
*   **Raises:** `OSError` on creation failure.

### `RasUtils.find_files_by_extension(extension, ras_object=None)`

*   **Purpose:** Lists all files within the initialized project directory matching a specific extension.
*   **Parameters:**
    *   `extension` (`str`): The file extension (e.g., '.prj', '.p*').
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`list` of `str`): List of full file paths.

### `RasUtils.get_file_size(file_path, ras_object=None)`

*   **Purpose:** Gets the size of a file in bytes.
*   **Parameters:**
    *   `file_path` (`Path`): Path to the file.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`int` or `None`): File size in bytes, or `None` if file not found.

### `RasUtils.get_file_modification_time(file_path, ras_object=None)`

*   **Purpose:** Gets the last modification timestamp of a file.
*   **Parameters:**
    *   `file_path` (`Path`): Path to the file.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`float` or `None`): Unix timestamp of last modification, or `None` if file not found.

### `RasUtils.get_plan_path(current_plan_number_or_path, ras_object=None)`

*   **Purpose:** Resolves a plan number or path string into a full, validated `Path` object for a plan file.
*   **Parameters:**
    *   `current_plan_number_or_path` (`str` or `Path`): Plan number (1-99) or full path.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`Path`): The validated path to the plan file.
*   **Raises:** `ValueError`, `TypeError`, `FileNotFoundError`.

### `RasUtils.remove_with_retry(path, max_attempts=5, initial_delay=1.0, is_folder=True, ras_object=None)`

*   **Purpose:** Safely removes a file or folder, retrying with exponential backoff if a `PermissionError` occurs.
*   **Parameters:**
    *   `path` (`Path`): Path to remove.
    *   `max_attempts` (`int`, optional): Max removal attempts. Default is 5.
    *   `initial_delay` (`float`, optional): Initial delay in seconds. Default is 1.0.
    *   `is_folder` (`bool`, optional): `True` if path is a folder, `False` if a file. Default is `True`.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** (`bool`): `True` if removal succeeded, `False` otherwise.

### `RasUtils.update_plan_file(plan_number_or_path, file_type, entry_number, ras_object=None)`

*   **Purpose:** Updates a line in a plan file to reference a different associated file (geometry, flow, unsteady).
*   **Parameters:**
    *   `plan_number_or_path` (`str` or `Path`): Plan number or full path.
    *   `file_type` (`str`): Type of file link to update ('Geom', 'Flow', 'Unsteady').
    *   `entry_number` (`int`): The number (1-99) of the new associated file.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the plan file and refreshes the `ras_object`.
*   **Raises:** `ValueError`, `FileNotFoundError`.

### `RasUtils.check_file_access(file_path, mode='r')`

*   **Purpose:** Verifies if a file exists and can be accessed with the specified mode ('r', 'w', etc.).
*   **Parameters:**
    *   `file_path` (`Path`): Path to the file.
    *   `mode` (`str`, optional): Access mode to check. Default is 'r'.
*   **Returns:** `None`.
*   **Raises:** `FileNotFoundError`, `PermissionError`.

### `RasUtils.convert_to_dataframe(data_source, **kwargs)`

*   **Purpose:** Converts various inputs (existing DataFrame, CSV, Excel, TSV, Parquet file path) into a pandas DataFrame.
*   **Parameters:**
    *   `data_source` (`pd.DataFrame` or `Path`): Input data or file path.
    *   `**kwargs`: Additional arguments for pandas read functions (e.g., `sheet_name` for Excel).
*   **Returns:** `pd.DataFrame`.
*   **Raises:** `NotImplementedError` for unsupported types.

### `RasUtils.save_to_excel(dataframe, excel_path, **kwargs)`

*   **Purpose:** Saves a DataFrame to an Excel file, with retries to handle potential file locking issues.
*   **Parameters:**
    *   `dataframe` (`pd.DataFrame`): DataFrame to save.
    *   `excel_path` (`Path`): Output Excel file path.
    *   `**kwargs`: Additional arguments for `DataFrame.to_excel()`.
*   **Returns:** `None`.
*   **Raises:** `IOError` if saving fails after retries.

### `RasUtils.calculate_rmse(observed_values, predicted_values, normalized=True)`

*   **Purpose:** Calculates Root Mean Squared Error (RMSE), optionally normalized.
*   **Parameters:**
    *   `observed_values` (`np.ndarray`): Array of actual values.
    *   `predicted_values` (`np.ndarray`): Array of predicted values.
    *   `normalized` (`bool`, optional): If `True`, normalize by the mean of observed values. Default is `True`.
*   **Returns:** (`float`): Calculated RMSE.

### `RasUtils.calculate_percent_bias(observed_values, predicted_values, as_percentage=False)`

*   **Purpose:** Calculates Percent Bias (PBIAS).
*   **Parameters:**
    *   `observed_values` (`np.ndarray`): Array of actual values.
    *   `predicted_values` (`np.ndarray`): Array of predicted values.
    *   `as_percentage` (`bool`, optional): If `True`, returns result multiplied by 100. Default is `False`.
*   **Returns:** (`float`): Calculated Percent Bias.

### `RasUtils.calculate_error_metrics(observed_values, predicted_values)`

*   **Purpose:** Calculates a dictionary of common error metrics (correlation, RMSE, Percent Bias).
*   **Parameters:**
    *   `observed_values` (`np.ndarray`): Array of actual values.
    *   `predicted_values` (`np.ndarray`): Array of predicted values.
*   **Returns:** `Dict[str, float]`: Dictionary with keys 'cor', 'rmse', 'pb'.

### `RasUtils.update_file(file_path, update_function, *args)`

*   **Purpose:** Generic function to read a file, apply a modification function to its lines, and write it back.
*   **Parameters:**
    *   `file_path` (`Path`): Path to the file.
    *   `update_function` (`Callable`): A function that takes a list of lines (and optionally `*args`) and returns a modified list of lines.
    *   `*args`: Additional arguments passed to `update_function`.
*   **Returns:** `None`.
*   **Raises:** Exceptions from file I/O or `update_function`.

### `RasUtils.get_next_number(existing_numbers)`

*   **Purpose:** Finds the smallest unused positive integer number given a list of existing numbers (as strings), returned as a zero-padded string. (Same as `RasPlan.get_next_number`)
*   **Parameters:**
    *   `existing_numbers` (`list` of `str`): List of existing numbers (e.g., ['01', '03']).
*   **Returns:** (`str`): The next available number (e.g., "02").

### `RasUtils.clone_file(template_path, new_path, update_function=None, *args)`

*   **Purpose:** Copies a template file to a new path and optionally applies a modification function to the new file's content.
*   **Parameters:**
    *   `template_path` (`Path`): Path to the source file.
    *   `new_path` (`Path`): Path for the new copied file.
    *   `update_function` (`Callable`, optional): Function to modify the lines of the new file. Takes a list of lines (and optionally `*args`).
    *   `*args`: Additional arguments for `update_function`.
*   **Returns:** `None`.
*   **Raises:** `FileNotFoundError` if template doesn't exist.

### `RasUtils.update_project_file(prj_file, file_type, new_num, ras_object=None)`

*   **Purpose:** Appends a new file entry line (e.g., `Plan File=p03`) to the project file (`.prj`).
*   **Parameters:**
    *   `prj_file` (`Path`): Path to the `.prj` file.
    *   `file_type` (`str`): Type of entry ('Plan', 'Geom', 'Flow', 'Unsteady').
    *   `new_num` (`str`): The two-digit number for the new entry (e.g., "03").
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `None`. Modifies the project file.

### `RasUtils.decode_byte_strings(dataframe)`

*   **Purpose:** Decodes all byte string (`b'...'`) columns in a DataFrame to UTF-8 strings.
*   **Parameters:**
    *   `dataframe` (`pd.DataFrame`): Input DataFrame.
*   **Returns:** `pd.DataFrame`: DataFrame with byte strings decoded.

### `RasUtils.perform_kdtree_query(reference_points, query_points, max_distance=2.0)`

*   **Purpose:** Finds the nearest point in `reference_points` for each point in `query_points` using KDTree, within a maximum distance.
*   **Parameters:**
    *   `reference_points` (`np.ndarray`): NxD array of reference points.
    *   `query_points` (`np.ndarray`): MxD array of query points.
    *   `max_distance` (`float`, optional): Maximum distance for a match. Default 2.0.
*   **Returns:** (`np.ndarray`): Array of length M containing indices of nearest reference points. Index is -1 if no point found within `max_distance`.

### `RasUtils.find_nearest_neighbors(points, max_distance=2.0)`

*   **Purpose:** Finds the nearest neighbor for each point within the same dataset using KDTree, excluding self-matches and points beyond `max_distance`.
*   **Parameters:**
    *   `points` (`np.ndarray`): NxD array of points.
    *   `max_distance` (`float`, optional): Maximum distance for a match. Default 2.0.
*   **Returns:** (`np.ndarray`): Array of length N containing indices of the nearest neighbor for each point. Index is -1 if no neighbor found within `max_distance`.

### `RasUtils.consolidate_dataframe(dataframe, group_by=None, pivot_columns=None, level=None, n_dimensional=False, aggregation_method='list')`

*   **Purpose:** Aggregates rows in a DataFrame based on grouping criteria, typically merging values into lists.
*   **Parameters:**
    *   `dataframe` (`pd.DataFrame`): Input DataFrame.
    *   `group_by` (`str` or `List[str]`, optional): Column(s) or index level(s) to group by.
    *   `pivot_columns` (`str` or `List[str]`, optional): Column(s) to use for pivoting (if `n_dimensional`).
    *   `level` (`int`, optional): Index level to group by.
    *   `n_dimensional` (`bool`, optional): Use `pivot_table` if `True`. Default `False`.
    *   `aggregation_method` (`str` or `Callable`, optional): How to aggregate ('list', 'sum', 'mean', etc.). Default 'list'.
*   **Returns:** `pd.DataFrame`: The consolidated DataFrame.

### `RasUtils.find_nearest_value(array, target_value)`

*   **Purpose:** Finds the element in an array that is numerically closest to a target value.
*   **Parameters:**
    *   `array` (`list` or `np.ndarray`): Array of numbers to search within.
    *   `target_value` (`int` or `float`): The value to find the nearest match for.
*   **Returns:** (`int` or `float`): The value from the array closest to `target_value`.

### `RasUtils.horizontal_distance(coord1, coord2)`

*   **Purpose:** Calculates the 2D Euclidean distance between two points.
*   **Parameters:**
    *   `coord1` (`np.ndarray`): [X, Y] coordinates of the first point.
    *   `coord2` (`np.ndarray`): [X, Y] coordinates of the second point.
*   **Returns:** (`float`): The horizontal distance.

---

## Class: RasExamples

Provides methods to download, manage, and access HEC-RAS example projects included with the official HEC-RAS releases. Useful for testing and demonstration.

### `RasExamples.get_example_projects(version_number='6.6')`

*   **Purpose:** Downloads the example projects zip file for a specific HEC-RAS version if it doesn't already exist locally. Initializes the class to read the zip file structure.
*   **Parameters:**
    *   `version_number` (`str`, optional): HEC-RAS version string (e.g., "6.6", "6.5"). Default is "6.6".
*   **Returns:** (`Path`): Path to the directory where projects will be extracted (`example_projects`).
*   **Raises:** `ValueError` for invalid version, `requests.exceptions.RequestException` on download failure.

### `RasExamples.list_categories()`

*   **Purpose:** Lists the categories (top-level folders) available in the example projects zip file.
*   **Parameters:** None.
*   **Returns:** (`List[str]`): List of category names.

### `RasExamples.list_projects(category=None)`

*   **Purpose:** Lists the project names available, optionally filtered by category.
*   **Parameters:**
    *   `category` (`str`, optional): If provided, lists projects only within this category.
*   **Returns:** (`List[str]`): List of project names.

### `RasExamples.extract_project(project_names)`

*   **Purpose:** Extracts one or more specified projects from the zip file into the `example_projects` directory. Overwrites if already extracted.
*   **Parameters:**
    *   `project_names` (`str` or `List[str]`): Name(s) of the project(s) to extract.
*   **Returns:** (`Path` or `List[Path]`): Path(s) to the extracted project folder(s). Returns a single `Path` if one name was given, a list otherwise.
*   **Raises:** `ValueError` if a project name is not found.

### `RasExamples.is_project_extracted(project_name)`

*   **Purpose:** Checks if a specific project has already been extracted into the `example_projects` directory.
*   **Parameters:**
    *   `project_name` (`str`): Name of the project to check.
*   **Returns:** (`bool`): `True` if the project folder exists, `False` otherwise.

### `RasExamples.clean_projects_directory()`

*   **Purpose:** Removes the entire `example_projects` directory and its contents, then recreates the empty directory.
*   **Parameters:** None.
*   **Returns:** `None`.

### `RasExamples.download_fema_ble_model(huc8, output_dir=None)`

*   **Purpose:** (Placeholder) Intended to download FEMA Base Level Engineering models. *Currently not implemented.*
*   **Parameters:**
    *   `huc8` (`str`): 8-digit HUC code.
    *   `output_dir` (`str`, optional): Directory to save files.
*   **Returns:** (`str`): Path to the extracted model directory.

---

## Class: RasCmdr

Contains static methods for executing HEC-RAS simulations. Assumes a `RasPrj` object (defaulting to global `ras`) is initialized.

### `RasCmdr.compute_plan(plan_number, dest_folder=None, ras_object=None, clear_geompre=False, num_cores=None, overwrite_dest=False)`

*   **Purpose:** Executes a single HEC-RAS plan computation using the command line. Optionally copies the project to a destination folder first.
*   **Parameters:**
    *   `plan_number` (`str` or `Path`): Plan number (e.g., "01") or full path to plan file.
    *   `dest_folder` (`str` or `Path`, optional): Folder name (relative to project parent) or full path for computation. If `None`, runs in the original project folder.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
    *   `clear_geompre` (`bool`, optional): Clear `.c*` files before running. Default `False`.
    *   `num_cores` (`int`, optional): Number of cores to set in the plan file before running. Default `None` (use plan's current setting).
    *   `overwrite_dest` (`bool`, optional): If `True`, overwrite `dest_folder` if it exists. Default `False`.
*   **Returns:** (`bool`): `True` if execution succeeded (process completed without error), `False` otherwise.
*   **Raises:** `ValueError` if `dest_folder` exists and `overwrite_dest` is False, `FileNotFoundError`, `PermissionError`, `subprocess.CalledProcessError`.

### `RasCmdr.compute_parallel(plan_number=None, max_workers=2, num_cores=2, clear_geompre=False, ras_object=None, dest_folder=None, overwrite_dest=False)`

*   **Purpose:** Executes multiple HEC-RAS plans in parallel by creating temporary worker copies of the project. Consolidates results into a final destination folder.
*   **Parameters:**
    *   `plan_number` (`str` or `List[str]`, optional): Plan number(s) to run. If `None`, runs all plans in the project.
    *   `max_workers` (`int`, optional): Max number of parallel HEC-RAS instances. Default 2.
    *   `num_cores` (`int`, optional): Cores assigned to *each* worker instance. Default 2.
    *   `clear_geompre` (`bool`, optional): Clear `.c*` files in worker folders. Default `False`.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
    *   `dest_folder` (`str` or `Path`, optional): Final folder for consolidated results. If `None`, creates `ProjectName [Computed]` folder.
    *   `overwrite_dest` (`bool`, optional): Overwrite `dest_folder` if it exists. Default `False`.
*   **Returns:** `Dict[str, bool]`: Dictionary mapping plan numbers to their execution success status (`True`/`False`).
*   **Raises:** `ValueError`, `FileNotFoundError`, `PermissionError`, `RuntimeError`.

### `RasCmdr.compute_test_mode(plan_number=None, dest_folder_suffix="[Test]", clear_geompre=False, num_cores=None, ras_object=None, overwrite_dest=False)`

*   **Purpose:** Executes specified HEC-RAS plans sequentially within a dedicated test folder (a copy of the project).
*   **Parameters:**
    *   `plan_number` (`str` or `List[str]`, optional): Plan number(s) to run. If `None`, runs all plans.
    *   `dest_folder_suffix` (`str`, optional): Suffix for the test folder name (e.g., `ProjectName [Test]`). Default "[Test]".
    *   `clear_geompre` (`bool`, optional): Clear `.c*` files before running each plan. Default `False`.
    *   `num_cores` (`int`, optional): Cores to set for each plan execution. Default `None`.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
    *   `overwrite_dest` (`bool`, optional): Overwrite test folder if it exists. Default `False`.
*   **Returns:** `Dict[str, bool]`: Dictionary mapping plan numbers to their execution success status (`True`/`False`).
*   **Raises:** `ValueError`, `FileNotFoundError`, `PermissionError`.

---

## Class: HdfBase

Contains fundamental static methods for interacting with HEC-RAS HDF files. Used by other `Hdf*` classes. Requires an open `h5py.File` object or uses `@standardize_input`.

### `HdfBase.get_simulation_start_time(hdf_file)`

*   **Purpose:** Extracts the simulation start time attribute from the Plan Information group.
*   **Parameters:**
    *   `hdf_file` (`h5py.File`): Open HDF file object.
*   **Returns:** (`datetime`): Simulation start time.
*   **Raises:** `ValueError` if path not found or time parsing fails.

### `HdfBase.get_unsteady_timestamps(hdf_file)`

*   **Purpose:** Extracts the list of unsteady output timestamps (usually in milliseconds format) and converts them to datetime objects.
*   **Parameters:**
    *   `hdf_file` (`h5py.File`): Open HDF file object.
*   **Returns:** `List[datetime]`: List of datetime objects for each output time step.

### `HdfBase.get_2d_flow_area_names_and_counts(hdf_path)`

*   **Purpose:** Gets the names and cell counts of all 2D Flow Areas defined in the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry HDF).
*   **Returns:** `List[Tuple[str, int]]`: List of tuples `(area_name, cell_count)`.
*   **Raises:** `ValueError` on read errors.

### `HdfBase.get_projection(hdf_path)`

*   **Purpose:** Retrieves the spatial projection information (WKT string) from the HDF file attributes or associated `.rasmap` file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file.
*   **Returns:** (`str` or `None`): Well-Known Text (WKT) string of the projection, or `None` if not found.

### `HdfBase.get_attrs(hdf_file, attr_path)`

*   **Purpose:** Retrieves all attributes from a specific group or dataset within the HDF file.
*   **Parameters:**
    *   `hdf_file` (`h5py.File`): Open HDF file object.
    *   `attr_path` (`str`): Internal HDF path to the group/dataset (e.g., "Plan Data/Plan Information").
*   **Returns:** `Dict[str, Any]`: Dictionary of attributes. Returns empty dict if path not found.

### `HdfBase.get_dataset_info(file_path, group_path='/')`

*   **Purpose:** Prints a recursive listing of the structure (groups, datasets, attributes, shapes, dtypes) within an HDF5 file, starting from `group_path`.
*   **Parameters:**
    *   `file_path` (Input handled by `@standardize_input`): Path identifier for the HDF file.
    *   `group_path` (`str`, optional): Internal HDF path to start exploration from. Default is root ('/').
*   **Returns:** `None`. Prints to console.

### `HdfBase.get_polylines_from_parts(hdf_path, path, info_name="Polyline Info", parts_name="Polyline Parts", points_name="Polyline Points")`

*   **Purpose:** Reconstructs Shapely LineString or MultiLineString geometries from HEC-RAS's standard polyline representation in HDF (using Info, Parts, Points datasets).
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file.
    *   `path` (`str`): Internal HDF base path containing the polyline datasets (e.g., "Geometry/River Centerlines").
    *   `info_name` (`str`, optional): Name of the dataset containing polyline start/count info. Default "Polyline Info".
    *   `parts_name` (`str`, optional): Name of the dataset defining parts for multi-part lines. Default "Polyline Parts".
    *   `points_name` (`str`, optional): Name of the dataset containing all point coordinates. Default "Polyline Points".
*   **Returns:** `List[LineString or MultiLineString]`: List of reconstructed Shapely geometries.

### `HdfBase.print_attrs(name, obj)`

*   **Purpose:** Helper method to print the attributes of an HDF5 object (Group or Dataset) during exploration (used by `get_dataset_info`).
*   **Parameters:**
    *   `name` (`str`): Name of the HDF5 object.
    *   `obj` (`h5py.Group` or `h5py.Dataset`): The HDF5 object.
*   **Returns:** `None`. Prints to console.

---

## Class: HdfBndry

Contains static methods for extracting boundary-related *geometry* features (BC Lines, Breaklines, Refinement Regions, Reference Lines/Points) from HEC-RAS HDF files (typically geometry HDF). Returns GeoDataFrames.

### `HdfBndry.get_bc_lines(hdf_path)`

*   **Purpose:** Extracts 2D Flow Area Boundary Condition Lines.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier (usually geometry HDF).
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries and attributes (Name, SA-2D, Type, etc.).

### `HdfBndry.get_breaklines(hdf_path)`

*   **Purpose:** Extracts 2D Flow Area Break Lines. Skips invalid (zero-length, single-point) breaklines.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier (usually geometry HDF).
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString/MultiLineString geometries and attributes (bl_id, Name).

### `HdfBndry.get_refinement_regions(hdf_path)`

*   **Purpose:** Extracts 2D Flow Area Refinement Regions.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier (usually geometry HDF).
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Polygon/MultiPolygon geometries and attributes (rr_id, Name).

### `HdfBndry.get_reference_lines(hdf_path, mesh_name=None)`

*   **Purpose:** Extracts Reference Lines used for profile output, optionally filtering by mesh name.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier (usually geometry HDF).
    *   `mesh_name` (`str`, optional): Filter results to this specific mesh area.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString/MultiLineString geometries and attributes (refln_id, Name, mesh_name, Type).

### `HdfBndry.get_reference_points(hdf_path, mesh_name=None)`

*   **Purpose:** Extracts Reference Points used for point output, optionally filtering by mesh name.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier (usually geometry HDF).
    *   `mesh_name` (`str`, optional): Filter results to this specific mesh area.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Point geometries and attributes (refpt_id, Name, mesh_name, Cell Index).

---

## Class: HdfFluvialPluvial

Contains static methods for analyzing fluvial-pluvial boundaries based on simulation results.

### `HdfFluvialPluvial.calculate_fluvial_pluvial_boundary(hdf_path, delta_t=12)`

*   **Purpose:** Calculates the boundary line between areas dominated by fluvial (riverine) vs. pluvial (rainfall/local) flooding, based on the timing difference of maximum water surface elevation between adjacent 2D cells. Attempts to join adjacent boundary segments.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF file.
    *   `delta_t` (`float`, optional): Time difference threshold in hours. Adjacent cells with max WSE time differences greater than this are considered part of the boundary. Default is 12.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame containing LineString geometries representing the calculated boundary. CRS matches the input HDF.
*   **Raises:** `ValueError` if required mesh or results data is missing.

---

## Class: HdfInfiltration

Contains static methods for handling infiltration data within HEC-RAS HDF files (typically geometry HDF).

### `HdfInfiltration.get_infiltration_baseoverrides(hdf_path: Path) -> Optional[pd.DataFrame]`

*   **Purpose:** Retrieves current infiltration parameters from a HEC-RAS geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `Optional[pd.DataFrame]`: DataFrame containing infiltration parameters if successful, None if operation fails.

### `HdfInfiltration.get_infiltration_layer_data(hdf_path: Path) -> Optional[pd.DataFrame]`

*   **Purpose:** Retrieves current infiltration parameters from a HEC-RAS infiltration layer HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the infiltration layer HDF.
*   **Returns:** `Optional[pd.DataFrame]`: DataFrame containing infiltration parameters if successful, None if operation fails.

### `HdfInfiltration.set_infiltration_layer_data(hdf_path: Path, infiltration_df: pd.DataFrame) -> Optional[pd.DataFrame]`

*   **Purpose:** Sets infiltration layer data in the infiltration layer HDF file directly from the provided DataFrame.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the infiltration layer HDF.
    *   `infiltration_df` (`pd.DataFrame`): DataFrame containing infiltration parameters.
*   **Returns:** `Optional[pd.DataFrame]`: The infiltration DataFrame if successful, None if operation fails.

### `HdfInfiltration.scale_infiltration_data(hdf_path: Path, infiltration_df: pd.DataFrame, scale_factors: Dict[str, float]) -> Optional[pd.DataFrame]`

*   **Purpose:** Updates infiltration parameters in the HDF file with scaling factors.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `infiltration_df` (`pd.DataFrame`): DataFrame containing infiltration parameters.
    *   `scale_factors` (`Dict[str, float]`): Dictionary mapping column names to their scaling factors.
*   **Returns:** `Optional[pd.DataFrame]`: The updated infiltration DataFrame if successful, None if operation fails.

### `HdfInfiltration.get_infiltration_map(hdf_path: Path = None, ras_object: Any = None) -> dict`

*   **Purpose:** Reads the infiltration raster map from HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file. If not provided, uses first infiltration_hdf_path from rasmap_df.
    *   `ras_object` (`RasPrj`, optional): Specific RAS object to use. If None, uses the global ras instance.
*   **Returns:** `dict`: Dictionary mapping raster values to mukeys.

### `HdfInfiltration.calculate_soil_statistics(zonal_stats: list, raster_map: dict) -> pd.DataFrame`

*   **Purpose:** Calculates soil statistics from zonal statistics.
*   **Parameters:**
    *   `zonal_stats` (`list`): List of zonal statistics.
    *   `raster_map` (`dict`): Dictionary mapping raster values to mukeys.
*   **Returns:** `pd.DataFrame`: DataFrame with soil statistics including percentages and areas.

### `HdfInfiltration.get_significant_mukeys(soil_stats: pd.DataFrame, threshold: float = 1.0) -> pd.DataFrame`

*   **Purpose:** Gets mukeys with percentage greater than threshold.
*   **Parameters:**
    *   `soil_stats` (`pd.DataFrame`): DataFrame with soil statistics.
    *   `threshold` (`float`, optional): Minimum percentage threshold. Default 1.0.
*   **Returns:** `pd.DataFrame`: DataFrame with significant mukeys and their statistics.

### `HdfInfiltration.calculate_total_significant_percentage(significant_mukeys: pd.DataFrame) -> float`

*   **Purpose:** Calculates total percentage covered by significant mukeys.
*   **Parameters:**
    *   `significant_mukeys` (`pd.DataFrame`): DataFrame of significant mukeys.
*   **Returns:** `float`: Total percentage covered by significant mukeys.

### `HdfInfiltration.save_statistics(soil_stats: pd.DataFrame, output_path: Path, include_timestamp: bool = True)`

*   **Purpose:** Saves soil statistics to CSV.
*   **Parameters:**
    *   `soil_stats` (`pd.DataFrame`): DataFrame with soil statistics.
    *   `output_path` (`Path`): Path to save CSV file.
    *   `include_timestamp` (`bool`, optional): Whether to include timestamp in filename. Default True.
*   **Returns:** None

### `HdfInfiltration.get_infiltration_parameters(hdf_path: Path = None, mukey: str = None, ras_object: Any = None) -> dict`

*   **Purpose:** Gets infiltration parameters for a specific mukey from HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file. If not provided, uses first infiltration_hdf_path from rasmap_df.
    *   `mukey` (`str`): Mukey identifier.
    *   `ras_object` (`RasPrj`, optional): Specific RAS object to use. If None, uses the global ras instance.
*   **Returns:** `dict`: Dictionary of infiltration parameters.

### `HdfInfiltration.calculate_weighted_parameters(soil_stats: pd.DataFrame, infiltration_params: dict) -> dict`

*   **Purpose:** Calculates weighted infiltration parameters based on soil statistics.
*   **Parameters:**
    *   `soil_stats` (`pd.DataFrame`): DataFrame with soil statistics.
    *   `infiltration_params` (`dict`): Dictionary of infiltration parameters by mukey.
*   **Returns:** `dict`: Dictionary of weighted average infiltration parameters.

---

## Class: HdfMesh

Contains static methods for extracting 2D mesh geometry information from HEC-RAS HDF files (typically geometry or plan HDF).

### `HdfMesh.get_mesh_area_names(hdf_path)`

*   **Purpose:** Retrieves the names of all 2D Flow Areas defined in the HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file.
*   **Returns:** `List[str]`: List of 2D Flow Area names.

### `HdfMesh.get_mesh_areas(hdf_path)`

*   **Purpose:** Extracts the outer perimeter polygons for each 2D Flow Area.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Polygon geometries and 'mesh_name' attribute.

### `HdfMesh.get_mesh_cell_polygons(hdf_path)`

*   **Purpose:** Reconstructs the individual cell polygons for all 2D Flow Areas by assembling cell faces.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Polygon geometries and attributes 'mesh_name', 'cell_id'.

### `HdfMesh.get_mesh_cell_points(hdf_path)`

*   **Purpose:** Extracts the center point coordinates for each cell in all 2D Flow Areas.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Point geometries and attributes 'mesh_name', 'cell_id'.

### `HdfMesh.get_mesh_cell_faces(hdf_path)`

*   **Purpose:** Extracts the face line segments that form the boundaries of the mesh cells.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries and attributes 'mesh_name', 'face_id'.

### `HdfMesh.get_mesh_area_attributes(hdf_path)`

*   **Purpose:** Retrieves the main attributes associated with the 2D Flow Areas group in the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `pd.DataFrame`: DataFrame containing the attributes (e.g., Manning's n values).

### `HdfMesh.get_mesh_face_property_tables(hdf_path)`

*   **Purpose:** Extracts the detailed hydraulic property tables (Elevation vs. Area, Wetted Perimeter, Roughness) associated with each *face* in each 2D Flow Area.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `Dict[str, pd.DataFrame]`: Dictionary mapping mesh names to DataFrames. Each DataFrame contains columns ['Face ID', 'Z', 'Area', 'Wetted Perimeter', "Manning's n"].

### `HdfMesh.get_mesh_cell_property_tables(hdf_path)`

*   **Purpose:** Extracts the detailed hydraulic property tables (Elevation vs. Volume, Surface Area) associated with each *cell* in each 2D Flow Area.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `Dict[str, pd.DataFrame]`: Dictionary mapping mesh names to DataFrames. Each DataFrame contains columns ['Cell ID', 'Z', 'Volume', 'Surface Area'].

---

## Class: HdfPipe

Contains static methods for handling pipe network geometry and results data from HEC-RAS HDF files.

### `HdfPipe.get_pipe_conduits(hdf_path, crs="EPSG:4326")`

*   **Purpose:** Extracts pipe conduit centerlines, attributes, and terrain profiles from the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry or plan HDF).
    *   `crs` (`str`, optional): Coordinate Reference System string. Default "EPSG:4326".
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries ('Polyline'), attributes, and 'Terrain_Profiles' (list of (station, elevation) tuples).

### `HdfPipe.get_pipe_nodes(hdf_path)`

*   **Purpose:** Extracts pipe node locations and attributes from the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry or plan HDF).
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Point geometries and attributes.

### `HdfPipe.get_pipe_network(hdf_path, pipe_network_name=None, crs="EPSG:4326")`

*   **Purpose:** Extracts the detailed geometry of a specific pipe network, including cell polygons, faces, nodes, and connectivity information from the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry or plan HDF).
    *   `pipe_network_name` (`str`, optional): Name of the network. If `None`, uses the first network found.
    *   `crs` (`str`, optional): Coordinate Reference System string. Default "EPSG:4326".
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame primarily representing cells (Polygon geometry), with related face and node info included as attributes or object columns.
*   **Raises:** `ValueError` if `pipe_network_name` not found.

### `HdfPipe.get_pipe_profile(hdf_path, conduit_id)`

*   **Purpose:** Extracts the station-elevation terrain profile data for a specific pipe conduit from the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry or plan HDF).
    *   `conduit_id` (`int`): Zero-based index of the conduit.
*   **Returns:** `pd.DataFrame`: DataFrame with columns ['Station', 'Elevation'].
*   **Raises:** `KeyError`, `IndexError`.

### `HdfPipe.get_pipe_network_timeseries(hdf_path, variable)`

*   **Purpose:** Extracts time series results for a specified variable across all elements (cells, faces, pipes, nodes) of a pipe network.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `variable` (`str`): The results variable name (e.g., "Cell Water Surface", "Pipes/Pipe Flow DS", "Nodes/Depth").
*   **Returns:** `xr.DataArray`: DataArray with dimensions ('time', 'location') containing the time series values. Includes units attribute.
*   **Raises:** `ValueError` for invalid variable name, `KeyError`.

### `HdfPipe.get_pipe_network_summary(hdf_path)`

*   **Purpose:** Extracts summary statistics (min/max values, timing) for pipe network results from the plan results HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `pd.DataFrame`: DataFrame containing the summary statistics. Returns empty DataFrame if data not found.
*   **Raises:** `KeyError`.

### `HdfPipe.extract_timeseries_for_node(plan_hdf_path, node_id)`

*   **Purpose:** Extracts time series data specifically for a single pipe node (Depth, Drop Inlet Flow, Water Surface).
*   **Parameters:**
    *   `plan_hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `node_id` (`int`): Zero-based index of the node.
*   **Returns:** `Dict[str, xr.DataArray]`: Dictionary mapping variable names to their respective DataArrays (time dimension only).

### `HdfPipe.extract_timeseries_for_conduit(plan_hdf_path, conduit_id)`

*   **Purpose:** Extracts time series data specifically for a single pipe conduit (Flow US/DS, Velocity US/DS).
*   **Parameters:**
    *   `plan_hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `conduit_id` (`int`): Zero-based index of the conduit.
*   **Returns:** `Dict[str, xr.DataArray]`: Dictionary mapping variable names to their respective DataArrays (time dimension only).

---

## Class: HdfPlan

Contains static methods for extracting general plan-level information and attributes from HEC-RAS HDF files (plan or geometry HDF).

### `HdfPlan.get_plan_start_time(hdf_path)`

*   **Purpose:** Gets the simulation start time from the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan HDF.
*   **Returns:** (`datetime`): Simulation start time.
*   **Raises:** `ValueError`.

### `HdfPlan.get_plan_end_time(hdf_path)`

*   **Purpose:** Gets the simulation end time from the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan HDF.
*   **Returns:** (`datetime`): Simulation end time.
*   **Raises:** `ValueError`.

### `HdfPlan.get_plan_timestamps_list(hdf_path)`

*   **Purpose:** Gets the list of simulation output timestamps from the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan HDF.
*   **Returns:** `List[datetime]`: List of output datetime objects.
*   **Raises:** `ValueError`.

### `HdfPlan.get_plan_information(hdf_path)`

*   **Purpose:** Extracts all attributes from the 'Plan Data/Plan Information' group in the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan HDF.
*   **Returns:** `Dict[str, Any]`: Dictionary of plan information attributes.
*   **Raises:** `ValueError`.

### `HdfPlan.get_plan_parameters(hdf_path)`

*   **Purpose:** Extracts all attributes from the 'Plan Data/Plan Parameters' group in the plan HDF file and returns them as a DataFrame. Includes the plan number extracted from the filename.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan HDF.
*   **Returns:** `pd.DataFrame`: DataFrame with columns ['Plan', 'Parameter', 'Value'].
*   **Raises:** `ValueError`.

### `HdfPlan.get_plan_met_precip(hdf_path)`

*   **Purpose:** Extracts precipitation attributes from the 'Event Conditions/Meteorology/Precipitation' group in the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan HDF.
*   **Returns:** `Dict[str, Any]`: Dictionary of precipitation attributes. Returns empty dict if not found.

### `HdfPlan.get_geometry_information(hdf_path)`

*   **Purpose:** Extracts root-level attributes (like Version, Units, Projection) from the 'Geometry' group in a geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `pd.DataFrame`: DataFrame with columns ['Value'] and index ['Attribute Name'].
*   **Raises:** `ValueError`.

---

## Class: HdfPlot

Contains static methods for creating basic plots from HEC-RAS HDF data using `matplotlib`.

### `HdfPlot.plot_mesh_cells(cell_polygons_df, projection, title='2D Flow Area Mesh Cells', figsize=(12, 8))`

*   **Purpose:** Plots 2D mesh cell outlines from a GeoDataFrame.
*   **Parameters:**
    *   `cell_polygons_df` (`gpd.GeoDataFrame`): GeoDataFrame containing cell polygons (requires 'geometry' column).
    *   `projection` (`str`): CRS string to assign if `cell_polygons_df` doesn't have one.
    *   `title` (`str`, optional): Plot title. Default '2D Flow Area Mesh Cells'.
    *   `figsize` (`Tuple[int, int]`, optional): Figure size. Default (12, 8).
*   **Returns:** (`gpd.GeoDataFrame` or `None`): The input GeoDataFrame (with CRS possibly assigned), or `None` if input was empty. Displays the plot.

### `HdfPlot.plot_time_series(df, x_col, y_col, title=None, figsize=(12, 6))`

*   **Purpose:** Creates a simple line plot for time series data from a DataFrame.
*   **Parameters:**
    *   `df` (`pd.DataFrame`): DataFrame containing the data.
    *   `x_col` (`str`): Column name for the x-axis (usually time).
    *   `y_col` (`str`): Column name for the y-axis.
    *   `title` (`str`, optional): Plot title. Default `None`.
    *   `figsize` (`Tuple[int, int]`, optional): Figure size. Default (12, 6).
*   **Returns:** `None`. Displays the plot.

---

## Class: HdfPump

Contains static methods for handling pump station geometry and results data from HEC-RAS HDF files.

### `HdfPump.get_pump_stations(hdf_path)`

*   **Purpose:** Extracts pump station locations and attributes from the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry or plan HDF).
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with Point geometries and attributes including 'station_id'.
*   **Raises:** `KeyError`.

### `HdfPump.get_pump_groups(hdf_path)`

*   **Purpose:** Extracts pump group attributes and efficiency curve data from the geometry HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`): Path identifier for the HDF file (usually geometry or plan HDF).
*   **Returns:** `pd.DataFrame`: DataFrame containing pump group attributes and 'efficiency_curve' data (list of values).
*   **Raises:** `KeyError`.

### `HdfPump.get_pump_station_timeseries(hdf_path, pump_station)`

*   **Purpose:** Extracts time series results (Flow, Stage HW, Stage TW, Pumps On) for a specific pump station from the plan results HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `pump_station` (`str`): Name of the pump station as defined in HEC-RAS.
*   **Returns:** `xr.DataArray`: DataArray with dimensions ('time', 'variable') containing the time series. Includes units attribute.
*   **Raises:** `KeyError`, `ValueError` if pump station not found.

### `HdfPump.get_pump_station_summary(hdf_path)`

*   **Purpose:** Extracts summary statistics (min/max values, volumes, durations) for all pump stations from the plan results HDF.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `pd.DataFrame`: DataFrame containing the summary statistics. Returns empty DataFrame if data not found.
*   **Raises:** `KeyError`.

### `HdfPump.get_pump_operation_timeseries(hdf_path, pump_station)`

*   **Purpose:** Extracts detailed pump operation time series data (similar to `get_pump_station_timeseries` but often from a different HDF group, potentially DSS Profile Output) for a specific pump station. Returns as a DataFrame.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `pump_station` (`str`): Name of the pump station.
*   **Returns:** `pd.DataFrame`: DataFrame with columns ['Time', 'Flow', 'Stage HW', 'Stage TW', 'Pump Station', 'Pumps on'].
*   **Raises:** `KeyError`, `ValueError` if pump station not found.

---

## Class: HdfResultsMesh

Contains static methods for extracting and analyzing 2D mesh *results* data from HEC-RAS plan HDF files.

### `HdfResultsMesh.get_mesh_summary(hdf_path, var, round_to="100ms")`

*   **Purpose:** Extracts summary output (e.g., max/min values and times) for a specific variable across all cells/faces in all 2D areas. Merges with geometry (points for cells, lines for faces).
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `var` (`str`): The summary variable name (e.g., "Maximum Water Surface", "Maximum Face Velocity", "Cell Last Iteration").
    *   `round_to` (`str`, optional): Time rounding precision for timestamps. Default "100ms".
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame containing the summary results, geometry, and mesh/element IDs.
*   **Raises:** `ValueError`.

### `HdfResultsMesh.get_mesh_timeseries(hdf_path, mesh_name, var, truncate=True)`

*   **Purpose:** Extracts the full time series for a specific variable for all cells or faces within a *single* specified 2D mesh area.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `mesh_name` (`str`): Name of the 2D Flow Area.
    *   `var` (`str`): Results variable name (e.g., "Water Surface", "Face Velocity", "Depth").
    *   `truncate` (`bool`, optional): If `True`, remove trailing zero-value time steps. Default `True`.
*   **Returns:** `xr.DataArray`: DataArray with dimensions ('time', 'cell_id' or 'face_id') containing the time series. Includes units attribute.
*   **Raises:** `ValueError`.

### `HdfResultsMesh.get_mesh_faces_timeseries(hdf_path, mesh_name)`

*   **Purpose:** Extracts time series for all standard *face-based* variables ("Face Velocity", "Face Flow") for a specific mesh area.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `mesh_name` (`str`): Name of the 2D Flow Area.
*   **Returns:** `xr.Dataset`: Dataset containing DataArrays for each face variable, indexed by time and face_id.

### `HdfResultsMesh.get_mesh_cells_timeseries(hdf_path, mesh_names=None, var=None, truncate=False, ras_object=None)`

*   **Purpose:** Extracts time series for specified (or all) *cell-based* variables for specified (or all) mesh areas.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `mesh_names` (`str` or `List[str]`, optional): Name(s) of mesh area(s). If `None`, processes all.
    *   `var` (`str`, optional): Specific variable name. If `None`, retrieves all available cell and face variables.
    *   `truncate` (`bool`, optional): Remove trailing zero time steps. Default `False`.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `Dict[str, xr.Dataset]`: Dictionary mapping mesh names to Datasets containing the requested variable(s) as DataArrays, indexed by time and cell_id/face_id.
*   **Raises:** `ValueError`.

### `HdfResultsMesh.get_mesh_last_iter(hdf_path)`

*   **Purpose:** Shortcut to get the summary output for "Cell Last Iteration".
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `pd.DataFrame`: DataFrame containing the last iteration count for each cell (via `get_mesh_summary`).

### `HdfResultsMesh.get_mesh_max_ws(hdf_path, round_to="100ms")`

*   **Purpose:** Shortcut to get the summary output for "Maximum Water Surface".
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `round_to` (`str`, optional): Time rounding precision. Default "100ms".
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame containing max WSE and time for each cell (via `get_mesh_summary`).

### `HdfResultsMesh.get_mesh_min_ws(hdf_path, round_to="100ms")`

*   **Purpose:** Shortcut to get the summary output for "Minimum Water Surface".
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `round_to` (`str`, optional): Time rounding precision. Default "100ms".
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame containing min WSE and time for each cell (via `get_mesh_summary`).

### `HdfResultsMesh.get_mesh_max_face_v(hdf_path, round_to="100ms")`

*   **Purpose:** Shortcut to get the summary output for "Maximum Face Velocity".
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `round_to` (`str`, optional): Time rounding precision. Default "100ms".
*   **Returns:** `pd.DataFrame`: DataFrame containing max velocity and time for each face (via `get_mesh_summary`).

### `HdfResultsMesh.get_mesh_min_face_v(hdf_path, round_to="100ms")`

*   **Purpose:** Shortcut to get the summary output for "Minimum Face Velocity".
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `round_to` (`str`, optional): Time rounding precision. Default "100ms".
*   **Returns:** `pd.DataFrame`: DataFrame containing min velocity and time for each face (via `get_mesh_summary`).

### `HdfResultsMesh.get_mesh_max_ws_err(hdf_path, round_to="100ms")`

*   **Purpose:** Shortcut to get the summary output for "Cell Maximum Water Surface Error".
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `round_to` (`str`, optional): Time rounding precision. Default "100ms".
*   **Returns:** `pd.DataFrame`: DataFrame containing max WSE error and time for each cell (via `get_mesh_summary`).

### `HdfResultsMesh.get_mesh_max_iter(hdf_path, round_to="100ms")`

*   **Purpose:** Shortcut to get the summary output for "Cell Last Iteration" (often used as max iteration indicator).
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `round_to` (`str`, optional): Time rounding precision. Default "100ms".
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame containing max iteration count and time for each cell (via `get_mesh_summary`).

---

## Class: HdfResultsPlan

Contains static methods for extracting general plan-level *results* and summary information from HEC-RAS plan HDF files.

### `HdfResultsPlan.get_unsteady_info(hdf_path)`

*   **Purpose:** Extracts attributes from the 'Results/Unsteady' group in the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `pd.DataFrame`: Single-row DataFrame containing the unsteady results attributes.
*   **Raises:** `FileNotFoundError`, `KeyError`, `RuntimeError`.

### `HdfResultsPlan.get_unsteady_summary(hdf_path)`

*   **Purpose:** Extracts attributes from the 'Results/Unsteady/Summary' group in the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `pd.DataFrame`: Single-row DataFrame containing the unsteady summary attributes.
*   **Raises:** `FileNotFoundError`, `KeyError`, `RuntimeError`.

### `HdfResultsPlan.get_volume_accounting(hdf_path)`

*   **Purpose:** Extracts attributes from the 'Results/Unsteady/Summary/Volume Accounting' group in the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** (`pd.DataFrame` or `None`): Single-row DataFrame containing volume accounting attributes, or `None` if the group doesn't exist.
*   **Raises:** `FileNotFoundError`, `RuntimeError`.

### `HdfResultsPlan.get_runtime_data(hdf_path)`

*   **Purpose:** Extracts detailed computational performance metrics (durations, speeds) for different simulation processes (Geometry, Preprocessing, Unsteady Flow) from the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** (`pd.DataFrame` or `None`): Single-row DataFrame containing runtime statistics, or `None` if data is missing or parsing fails.

### `HdfResultsPlan.get_reference_timeseries(hdf_path, reftype)`

*   **Purpose:** Extracts time series results for all Reference Lines or Reference Points from the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `reftype` (`str`): Type of reference feature ('lines' or 'points').
*   **Returns:** `pd.DataFrame`: DataFrame containing time series data for the specified reference type. Each column represents a reference feature, indexed by time step. Returns empty DataFrame if data not found.

### `HdfResultsPlan.get_reference_summary(hdf_path, reftype)`

*   **Purpose:** Extracts summary results (e.g., max/min values) for all Reference Lines or Reference Points from the plan HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
    *   `reftype` (`str`): Type of reference feature ('lines' or 'points').
*   **Returns:** `pd.DataFrame`: DataFrame containing summary data for the specified reference type. Returns empty DataFrame if data not found.

---

## Class: HdfResultsPlot

Contains static methods for plotting specific HEC-RAS *results* data using `matplotlib`.

### `HdfResultsPlot.plot_results_max_wsel(max_ws_df)`

*   **Purpose:** Creates a scatter plot showing the spatial distribution of maximum water surface elevation (WSE) per mesh cell.
*   **Parameters:**
    *   `max_ws_df` (`gpd.GeoDataFrame`): GeoDataFrame containing max WSE results (requires 'geometry' and 'maximum_water_surface' columns, typically from `HdfResultsMesh.get_mesh_max_ws`).
*   **Returns:** `None`. Displays the plot.

### `HdfResultsPlot.plot_results_max_wsel_time(max_ws_df)`

*   **Purpose:** Creates a scatter plot showing the spatial distribution of the *time* at which maximum water surface elevation occurred for each mesh cell. Also prints timing statistics.
*   **Parameters:**
    *   `max_ws_df` (`gpd.GeoDataFrame`): GeoDataFrame containing max WSE results (requires 'geometry' and 'maximum_water_surface_time' columns, typically from `HdfResultsMesh.get_mesh_max_ws`).
*   **Returns:** `None`. Displays the plot and prints statistics.

### `HdfResultsPlot.plot_results_mesh_variable(variable_df, variable_name, colormap='viridis', point_size=10)`

*   **Purpose:** Creates a generic scatter plot for visualizing any scalar mesh variable (e.g., max depth, max velocity) spatially across cell points.
*   **Parameters:**
    *   `variable_df` (`gpd.GeoDataFrame` or `pd.DataFrame`): (Geo)DataFrame containing the variable data and either a 'geometry' column (Point) or 'x', 'y' columns.
    *   `variable_name` (`str`): The name of the column in `variable_df` containing the data to plot and label.
    *   `colormap` (`str`, optional): Matplotlib colormap name. Default 'viridis'.
    *   `point_size` (`int`, optional): Size of scatter plot points. Default 10.
*   **Returns:** `None`. Displays the plot.
*   **Raises:** `ValueError` if coordinates or variable column are missing.

---

## Class: HdfResultsXsec

Contains static methods for extracting 1D cross-section and related *results* data from HEC-RAS plan HDF files.

### `HdfResultsXsec.get_xsec_timeseries(hdf_path)`

*   **Purpose:** Extracts time series results (Water Surface, Velocity, Flow, etc.) for all 1D cross-sections. Includes cross-section attributes (River, Reach, Station) and calculated maximum values as coordinates/variables.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `xr.Dataset`: Dataset containing DataArrays for each variable, indexed by time and cross_section name/identifier. Includes coordinates for attributes and max values.
*   **Raises:** `KeyError`.

### `HdfResultsXsec.get_ref_lines_timeseries(hdf_path)`

*   **Purpose:** Extracts time series results (Flow, Velocity, Water Surface) for all 1D Reference Lines.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `xr.Dataset`: Dataset containing DataArrays for each variable, indexed by time and reference line ID/name. Returns empty dataset if data not found.
*   **Raises:** `FileNotFoundError`, `KeyError`.

### `HdfResultsXsec.get_ref_points_timeseries(hdf_path)`

*   **Purpose:** Extracts time series results (Flow, Velocity, Water Surface) for all 1D Reference Points.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='plan_hdf'`): Path identifier for the plan results HDF.
*   **Returns:** `xr.Dataset`: Dataset containing DataArrays for each variable, indexed by time and reference point ID/name. Returns empty dataset if data not found.
*   **Raises:** `FileNotFoundError`, `KeyError`.

---

## Class: HdfStruc

Contains static methods for extracting hydraulic structure *geometry* data from HEC-RAS HDF files (typically geometry HDF).

### `HdfStruc.get_structures(hdf_path, datetime_to_str=False)`

*   **Purpose:** Extracts geometry and attributes for all structures (bridges, culverts, inline structures, lateral structures) defined in the geometry HDF. Includes centerline geometry, profile data, and other specific attributes like bridge coefficients.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `datetime_to_str` (`bool`, optional): Convert datetime attributes to ISO strings. Default `False`.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries (centerlines) and numerous attribute columns, including nested profile data ('Profile_Data'). Returns empty GeoDataFrame if no structures found.

### `HdfStruc.get_geom_structures_attrs(hdf_path)`

*   **Purpose:** Extracts the top-level attributes associated with the 'Geometry/Structures' group in the geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
*   **Returns:** `pd.DataFrame`: Single-row DataFrame containing the group attributes. Returns empty DataFrame if group not found.

---

## Class: HdfUtils

Contains general static utility methods used for HDF processing, data conversion, and calculations.

### `HdfUtils.convert_ras_string(value)`

*   **Purpose:** Converts byte strings or regular strings potentially containing HEC-RAS specific formats (dates, durations, booleans) into appropriate Python objects (`bool`, `datetime`, `List[datetime]`, `timedelta`, `str`).
*   **Parameters:**
    *   `value` (`str` or `bytes`): Input string or byte string.
*   **Returns:** (`bool`, `datetime`, `List[datetime]`, `timedelta`, `str`): Converted Python object.

### `HdfUtils.convert_ras_hdf_value(value)`

*   **Purpose:** General converter for values read directly from HDF datasets (handles `np.nan`, byte strings, numpy types).
*   **Parameters:**
    *   `value` (`Any`): Value read from HDF.
*   **Returns:** (`None`, `bool`, `str`, `List[str]`, `int`, `float`, `List[int]`, `List[float]`): Converted Python object.

### `HdfUtils.convert_df_datetimes_to_str(df)`

*   **Purpose:** Converts all columns of dtype `datetime64` in a DataFrame to ISO format strings (`YYYY-MM-DD HH:MM:SS`).
*   **Parameters:**
    *   `df` (`pd.DataFrame`): Input DataFrame.
*   **Returns:** `pd.DataFrame`: DataFrame with datetime columns converted to strings.

### `HdfUtils.convert_hdf5_attrs_to_dict(attrs, prefix=None)`

*   **Purpose:** Converts HDF5 attributes (from `.attrs`) into a Python dictionary, applying `convert_ras_hdf_value` to each value.
*   **Parameters:**
    *   `attrs` (`h5py.AttributeManager` or `Dict`): Attributes object or dictionary.
    *   `prefix` (`str`, optional): Prefix to add to keys in the resulting dictionary.
*   **Returns:** `Dict[str, Any]`: Dictionary of converted attributes.

### `HdfUtils.convert_timesteps_to_datetimes(timesteps, start_time, time_unit="days", round_to="100ms")`

*   **Purpose:** Converts an array of numeric time steps (relative to a start time) into a pandas `DatetimeIndex`.
*   **Parameters:**
    *   `timesteps` (`np.ndarray`): Array of time step values.
    *   `start_time` (`datetime`): The reference start datetime.
    *   `time_unit` (`str`, optional): Unit of the `timesteps` ('days' or 'hours'). Default 'days'.
    *   `round_to` (`str`, optional): Pandas frequency string for rounding. Default '100ms'.
*   **Returns:** `pd.DatetimeIndex`: Index of datetime objects.

### `HdfUtils.perform_kdtree_query(reference_points, query_points, max_distance=2.0)`

*   **Purpose:** Finds nearest point in `reference_points` for each point in `query_points` using KDTree, within `max_distance`. Returns index or -1. (See `RasUtils` for identical function).
*   **Parameters:** See `RasUtils.perform_kdtree_query`.
*   **Returns:** (`np.ndarray`): Array of indices or -1.

### `HdfUtils.find_nearest_neighbors(points, max_distance=2.0)`

*   **Purpose:** Finds nearest neighbor for each point within the same dataset using KDTree, excluding self and points beyond `max_distance`. Returns index or -1. (See `RasUtils` for identical function).
*   **Parameters:** See `RasUtils.find_nearest_neighbors`.
*   **Returns:** (`np.ndarray`): Array of indices or -1.

### `HdfUtils.parse_ras_datetime(datetime_str)`

*   **Purpose:** Parses HEC-RAS standard datetime string format ("ddMMMYYYY HH:MM:SS").
*   **Parameters:**
    *   `datetime_str` (`str`): String to parse.
*   **Returns:** (`datetime`): Parsed datetime object.

### `HdfUtils.parse_ras_window_datetime(datetime_str)`

*   **Purpose:** Parses HEC-RAS simulation window datetime string format ("ddMMMYYYY HHMM").
*   **Parameters:**
    *   `datetime_str` (`str`): String to parse.
*   **Returns:** (`datetime`): Parsed datetime object.

### `HdfUtils.parse_duration(duration_str)`

*   **Purpose:** Parses HEC-RAS duration string format ("HH:MM:SS").
*   **Parameters:**
    *   `duration_str` (`str`): String to parse.
*   **Returns:** (`timedelta`): Parsed timedelta object.

### `HdfUtils.parse_ras_datetime_ms(datetime_str)`

*   **Purpose:** Parses HEC-RAS datetime string format that includes milliseconds ("ddMMMYYYY HH:MM:SS:fff").
*   **Parameters:**
    *   `datetime_str` (`str`): String to parse.
*   **Returns:** (`datetime`): Parsed datetime object with microseconds.

### `HdfUtils.parse_run_time_window(window)`

*   **Purpose:** Parses a HEC-RAS time window string ("datetime1 to datetime2") into start and end datetime objects.
*   **Parameters:**
    *   `window` (`str`): Time window string.
*   **Returns:** `Tuple[datetime, datetime]`: Tuple containing (start_datetime, end_datetime).

### `HdfUtils.decode_byte_strings(dataframe)`

*   **Purpose:** Decodes all byte string (`b'...'`) columns in a DataFrame to UTF-8 strings. (See `RasUtils` for identical function).
*   **Parameters:** See `RasUtils.decode_byte_strings`.
*   **Returns:** `pd.DataFrame`.

### `HdfUtils.consolidate_dataframe(...)`

*   **Purpose:** Aggregates rows in a DataFrame based on grouping criteria. (See `RasUtils` for identical function).
*   **Parameters:** See `RasUtils.consolidate_dataframe`.
*   **Returns:** `pd.DataFrame`.

### `HdfUtils.find_nearest_value(...)`

*   **Purpose:** Finds the element in an array numerically closest to a target value. (See `RasUtils` for identical function).
*   **Parameters:** See `RasUtils.find_nearest_value`.
*   **Returns:** (`int` or `float`).

### `HdfUtils.horizontal_distance(...)`

*   **Purpose:** Calculates the 2D Euclidean distance between two points. (See `RasUtils` for identical function).
*   **Parameters:** See `RasUtils.horizontal_distance`.
*   **Returns:** (`float`).

---

## Class: HdfXsec

Contains static methods for extracting 1D cross-section *geometry* data from HEC-RAS HDF files (typically geometry HDF).

### `HdfXsec.get_cross_sections(hdf_path, datetime_to_str=True, ras_object=None)`

*   **Purpose:** Extracts detailed cross-section geometry, attributes, station-elevation data, Manning's n values, and ineffective flow areas from the geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `datetime_to_str` (`bool`, optional): Convert datetime attributes to strings. Default `True`.
    *   `ras_object` (`RasPrj`, optional): Instance for context. Defaults to global `ras`.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries (cross-section cut lines) and numerous attributes including nested lists/dicts for profile data ('station_elevation'), roughness ('mannings_n'), and ineffective areas ('ineffective_blocks').

### `HdfXsec.get_river_centerlines(hdf_path, datetime_to_str=False)`

*   **Purpose:** Extracts river centerline geometries and attributes from the geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `datetime_to_str` (`bool`, optional): Convert datetime attributes to strings. Default `False`.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries and attributes like 'River Name', 'Reach Name', 'length'.

### `HdfXsec.get_river_stationing(centerlines_gdf)`

*   **Purpose:** Calculates stationing values along river centerlines, interpolating points and determining direction based on upstream/downstream connections.
*   **Parameters:**
    *   `centerlines_gdf` (`gpd.GeoDataFrame`): GeoDataFrame obtained from `get_river_centerlines`.
*   **Returns:** `gpd.GeoDataFrame`: The input GeoDataFrame with added columns: 'station_start', 'station_end', 'stations' (array), 'points' (array of Shapely Points).

### `HdfXsec.get_river_reaches(hdf_path, datetime_to_str=False)`

*   **Purpose:** Extracts 1D river reach lines (often identical to centerlines but potentially simplified) from the geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `datetime_to_str` (`bool`, optional): Convert datetime attributes to strings. Default `False`.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries and attributes.

### `HdfXsec.get_river_edge_lines(hdf_path, datetime_to_str=False)`

*   **Purpose:** Extracts river edge lines (representing the extent of the 1D river schematic) from the geometry HDF file. Usually includes Left and Right edges.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `datetime_to_str` (`bool`, optional): Convert datetime attributes to strings. Default `False`.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries and attributes including 'bank_side' ('Left'/'Right').

### `HdfXsec.get_river_bank_lines(hdf_path, datetime_to_str=False)`

*   **Purpose:** Extracts river bank lines (defining the main channel within the cross-section) from the geometry HDF file.
*   **Parameters:**
    *   `hdf_path` (Input handled by `@standardize_input`, `file_type='geom_hdf'`): Path identifier for the geometry HDF.
    *   `datetime_to_str` (`bool`, optional): Convert datetime attributes to strings. Default `False`.
*   **Returns:** `gpd.GeoDataFrame`: GeoDataFrame with LineString geometries and attributes 'bank_id', 'bank_side'.

---

## Logging Configuration Functions

### `get_logger(name)`

*   **Purpose:** Retrieves a configured logger instance for use within the library or user scripts. Ensures logging is set up.
*   **Parameters:**
    *   `name` (`str`): Name for the logger (typically `__name__`).
*   **Returns:** (`logging.Logger`): A standard Python logger instance.

---

## Class: RasMap

Contains static methods for parsing and accessing information from HEC-RAS mapper configuration files (.rasmap) and automating post-processing tasks.

### `RasMap.parse_rasmap(rasmap_path: Union[str, Path], ras_object=None) -> pd.DataFrame`

*   **Purpose:** Parse a .rasmap file and extract relevant information, including paths to terrain, soil layers, land cover, and other spatial datasets.
*   **Parameters:**
    *   `rasmap_path` (`Union[str, Path]`): Path to the .rasmap file.
    *   `ras_object` (`RasPrj`, optional): Specific RAS object to use. If None, uses the global ras instance.
*   **Returns:** `pd.DataFrame`: A single-row DataFrame containing extracted information from the .rasmap file.
*   **Raises:** Various exceptions for file access or parsing failures.

### `RasMap.get_rasmap_path(ras_object=None) -> Optional[Path]`

*   **Purpose:** Get the path to the .rasmap file based on the current project.
*   **Parameters:**
    *   `ras_object` (`RasPrj`, optional): Specific RAS object to use. If None, uses the global ras instance.
*   **Returns:** `Optional[Path]`: Path to the .rasmap file if found, None otherwise.

### `RasMap.initialize_rasmap_df(ras_object=None) -> pd.DataFrame`

*   **Purpose:** Initialize the `rasmap_df` as part of project initialization. This is typically called internally by `init_ras_project`.
*   **Parameters:**
    *   `ras_object` (`RasPrj`, optional): Specific RAS object to use. If None, uses the global ras instance.
*   **Returns:** `pd.DataFrame`: DataFrame containing information from the .rasmap file.

### `RasMap.get_terrain_names(rasmap_path: Union[str, Path]) -> List[str]`
*   **Purpose:** Extracts all terrain layer names from a given `.rasmap` file.
*   **Parameters:**
    *   `rasmap_path` (`Union[str, Path]`): Path to the `.rasmap` file.
*   **Returns:** (`List[str]`): A list of terrain names.
*   **Raises:** `FileNotFoundError`, `ValueError`.

### `RasMap.postprocess_stored_maps(plan_number: str, specify_terrain: Optional[str] = None, layers: Union[str, List[str]] = None, ras_object: Optional[Any] = None) -> bool`
*   **Purpose:** Automates the generation of stored floodplain map outputs (e.g., `.tif` files) for a specific plan.
*   **Parameters:**
    *   `plan_number` (`str`): The plan to generate maps for.
    *   `specify_terrain` (`str`, optional): The name of a specific terrain to use for mapping. If provided, other terrains are temporarily ignored.
    *   `layers` (`Union[str, List[str]]`, optional): A list of map layers to generate. Defaults to `['WSEL', 'Velocity', 'Depth']`.
    *   `ras_object` (`RasPrj`, optional): The RAS project object to use.
*   **Returns:** (`bool`): `True` if the process completed successfully, `False` otherwise.
*   **Workflow:**
    1.  Backs up the original plan and `.rasmap` files.
    2.  Modifies the plan file to only run floodplain mapping.
    3.  Modifies the `.rasmap` file to include the specified stored map layers.
    4.  Runs `RasCmdr.compute_plan` to generate the maps.
    5.  Restores the original plan and `.rasmap` files, but keeps the newly added map layer definitions in the `.rasmap` file for future use.
==================================================

File: c:\GH\ras-commander-mcp-main\ras-commander reference files\HdfPlan.py
==================================================
"""
Class: HdfPlan

Attribution: A substantial amount of code in this file is sourced or derived 
from the https://github.com/fema-ffrd/rashdf library, 
released under MIT license and Copyright (c) 2024 fema-ffrd

The file has been forked and modified for use in RAS Commander.

-----

All of the methods in this class are static and are designed to be used without instantiation.


- get_plan_start_time()
- get_plan_end_time()
- get_plan_timestamps_list()     
- get_plan_information()
- get_plan_parameters()
- get_plan_met_precip()
- get_geometry_information()






"""

import h5py
import pandas as pd
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
import re
import numpy as np

from .HdfBase import HdfBase
from .HdfUtils import HdfUtils
from .Decorators import standardize_input, log_call
from .LoggingConfig import setup_logging, get_logger

logger = get_logger(__name__)


class HdfPlan:
    """
    A class for handling HEC-RAS plan HDF files.

    Provides static methods for extracting data from HEC-RAS plan HDF files including 
    simulation times, plan information, and geometry attributes. All methods use 
    @standardize_input for handling different input types and @log_call for logging.

    Note: This code is partially derived from the rashdf library (https://github.com/fema-ffrd/rashdf)
    under MIT license.
    """

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_plan_start_time(hdf_path: Path) -> datetime:
        """
        Get the plan start time from the plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            datetime: The plan start time in UTC format.

        Raises:
            ValueError: If there's an error reading the plan start time.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                return HdfBase.get_simulation_start_time(hdf_file)
        except Exception as e:
            raise ValueError(f"Failed to get plan start time: {str(e)}")

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_plan_end_time(hdf_path: Path) -> datetime:
        """
        Get the plan end time from the plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            datetime: The plan end time.

        Raises:
            ValueError: If there's an error reading the plan end time.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                plan_info = hdf_file.get('Plan Data/Plan Information')
                if plan_info is None:
                    raise ValueError("Plan Information not found in HDF file")
                time_str = plan_info.attrs.get('Simulation End Time')
                return HdfUtils.parse_ras_datetime(time_str.decode('utf-8'))
        except Exception as e:
            raise ValueError(f"Failed to get plan end time: {str(e)}")

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_plan_timestamps_list(hdf_path: Path) -> List[datetime]:
        """
        Get the list of output timestamps from the plan simulation.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            List[datetime]: Chronological list of simulation output timestamps in UTC.

        Raises:
            ValueError: If there's an error retrieving the plan timestamps.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                return HdfBase.get_unsteady_timestamps(hdf_file)
        except Exception as e:
            raise ValueError(f"Failed to get plan timestamps: {str(e)}")

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_plan_information(hdf_path: Path) -> Dict:
        """
        Get plan information from a HEC-RAS HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            Dict: Plan information including simulation times, flow regime, 
                computation settings, etc.

        Raises:
            ValueError: If there's an error retrieving the plan information.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                plan_info_path = "Plan Data/Plan Information"
                if plan_info_path not in hdf_file:
                    raise ValueError(f"Plan Information not found in {hdf_path}")
                
                attrs = {}
                for key in hdf_file[plan_info_path].attrs.keys():
                    value = hdf_file[plan_info_path].attrs[key]
                    if isinstance(value, bytes):
                        value = HdfUtils.convert_ras_string(value)
                    attrs[key] = value
                
                return attrs
        except Exception as e:
            raise ValueError(f"Failed to get plan information attributes: {str(e)}")

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_plan_parameters(hdf_path: Path) -> pd.DataFrame:
        """
        Get plan parameter attributes from a HEC-RAS HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            pd.DataFrame: A DataFrame containing the plan parameters with columns:
                - Parameter: Name of the parameter
                - Value: Value of the parameter (decoded if byte string)
                - Plan: Plan number (01-99) extracted from the filename (ProjectName.pXX.hdf)

        Raises:
            ValueError: If there's an error retrieving the plan parameter attributes.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                plan_params_path = "Plan Data/Plan Parameters"
                if plan_params_path not in hdf_file:
                    raise ValueError(f"Plan Parameters not found in {hdf_path}")
                
                # Extract parameters
                params_dict = {}
                for key in hdf_file[plan_params_path].attrs.keys():
                    value = hdf_file[plan_params_path].attrs[key]
                    
                    # Handle different types of values
                    if isinstance(value, bytes):
                        value = HdfUtils.convert_ras_string(value)
                    elif isinstance(value, np.ndarray):
                        # Handle array values
                        if value.dtype.kind in {'S', 'a'}:  # Array of byte strings
                            value = [v.decode('utf-8') if isinstance(v, bytes) else v for v in value]
                        else:
                            value = value.tolist()  # Convert numpy array to list
                        
                        # If it's a single-item list, extract the value
                        if len(value) == 1:
                            value = value[0]
                    
                    params_dict[key] = value
                
                # Create DataFrame from parameters
                df = pd.DataFrame.from_dict(params_dict, orient='index', columns=['Value'])
                df.index.name = 'Parameter'
                df = df.reset_index()
                
                # Extract plan number from filename
                filename = Path(hdf_path).name
                plan_match = re.search(r'\.p(\d{2})\.', filename)
                if plan_match:
                    plan_num = plan_match.group(1)
                else:
                    plan_num = "00"  # Default if no match found
                    logger.warning(f"Could not extract plan number from filename: {filename}")
                
                df['Plan'] = plan_num
                
                # Reorder columns to put Plan first
                df = df[['Plan', 'Parameter', 'Value']]
                
                return df

        except Exception as e:
            raise ValueError(f"Failed to get plan parameter attributes: {str(e)}")

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_plan_met_precip(hdf_path: Path) -> Dict:
        """
        Get precipitation attributes from a HEC-RAS HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            Dict: Precipitation attributes including method, time series data,
                and spatial distribution if available. Returns empty dict if
                no precipitation data exists.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                precip_path = "Event Conditions/Meteorology/Precipitation"
                if precip_path not in hdf_file:
                    logger.error(f"Precipitation data not found in {hdf_path}")
                    return {}
                
                attrs = {}
                for key in hdf_file[precip_path].attrs.keys():
                    value = hdf_file[precip_path].attrs[key]
                    if isinstance(value, bytes):
                        value = HdfUtils.convert_ras_string(value)
                    attrs[key] = value
                
                return attrs
        except Exception as e:
            logger.error(f"Failed to get precipitation attributes: {str(e)}")
            return {}
        
    @staticmethod
    @log_call
    @standardize_input(file_type='geom_hdf')
    def get_geometry_information(hdf_path: Path) -> pd.DataFrame:
        """
        Get root level geometry attributes from the HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            pd.DataFrame: DataFrame with geometry attributes including Creation Date/Time,
                        Version, Units, and Projection information.

        Raises:
            ValueError: If Geometry group is missing or there's an error reading attributes.
        """
        logger.info(f"Getting geometry attributes from {hdf_path}")
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                geom_attrs_path = "Geometry"
                logger.info(f"Checking for Geometry group in {hdf_path}")
                if geom_attrs_path not in hdf_file:
                    logger.error(f"Geometry group not found in {hdf_path}")
                    raise ValueError(f"Geometry group not found in {hdf_path}")

                attrs = {}
                geom_group = hdf_file[geom_attrs_path]
                logger.info("Getting root level geometry attributes")
                # Get root level geometry attributes only
                for key, value in geom_group.attrs.items():
                    if isinstance(value, bytes):
                        try:
                            value = HdfUtils.convert_ras_string(value)
                        except UnicodeDecodeError:
                            logger.warning(f"Failed to decode byte string for root attribute {key}")
                            continue
                    attrs[key] = value
                    logger.debug(f"Geometry attribute: {key} = {value}")

                logger.info(f"Successfully extracted {len(attrs)} root level geometry attributes")
                return pd.DataFrame.from_dict(attrs, orient='index', columns=['Value'])

        except (OSError, RuntimeError) as e:
            logger.error(f"Failed to read HDF file {hdf_path}: {str(e)}")
            raise ValueError(f"Failed to read HDF file {hdf_path}: {str(e)}")
        except Exception as e:
            logger.error(f"Failed to get geometry attributes: {str(e)}")
            raise ValueError(f"Failed to get geometry attributes: {str(e)}")



==================================================

File: c:\GH\ras-commander-mcp-main\ras-commander reference files\HdfResultsPlan.py
==================================================
"""
HdfResultsPlan: A module for extracting and analyzing HEC-RAS plan HDF file results.

Attribution:
    Substantial code sourced/derived from https://github.com/fema-ffrd/rashdf
    Copyright (c) 2024 fema-ffrd, MIT license

Description:
    Provides static methods for extracting unsteady flow results, volume accounting,
    and reference data from HEC-RAS plan HDF files.

Available Functions:
    - get_unsteady_info: Extract unsteady attributes
    - get_unsteady_summary: Extract unsteady summary data
    - get_volume_accounting: Extract volume accounting data
    - get_runtime_data: Extract runtime and compute time data

Note:
    All methods are static and designed to be used without class instantiation.
"""

from typing import Dict, List, Union, Optional
from pathlib import Path
import h5py
import pandas as pd
import xarray as xr
from .Decorators import standardize_input, log_call
from .HdfUtils import HdfUtils
from .HdfResultsXsec import HdfResultsXsec
from .LoggingConfig import get_logger
import numpy as np
from datetime import datetime
from .RasPrj import ras

logger = get_logger(__name__)


class HdfResultsPlan:
    """
    Handles extraction of results data from HEC-RAS plan HDF files.

    This class provides static methods for accessing and analyzing:
        - Unsteady flow results
        - Volume accounting data
        - Runtime statistics
        - Reference line/point time series outputs

    All methods use:
        - @standardize_input decorator for consistent file path handling
        - @log_call decorator for operation logging
        - HdfUtils class for common HDF operations

    Note:
        No instantiation required - all methods are static.
    """

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_unsteady_info(hdf_path: Path) -> pd.DataFrame:
        """
        Get unsteady attributes from a HEC-RAS HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.
            ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
            pd.DataFrame: A DataFrame containing the decoded unsteady attributes.

        Raises:
            FileNotFoundError: If the specified HDF file is not found.
            KeyError: If the "Results/Unsteady" group is not found in the HDF file.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                if "Results/Unsteady" not in hdf_file:
                    raise KeyError("Results/Unsteady group not found in the HDF file.")
                
                # Create dictionary from attributes and decode byte strings
                attrs_dict = {}
                for key, value in dict(hdf_file["Results/Unsteady"].attrs).items():
                    if isinstance(value, bytes):
                        attrs_dict[key] = value.decode('utf-8')
                    else:
                        attrs_dict[key] = value
                
                # Create DataFrame with a single row index
                return pd.DataFrame(attrs_dict, index=[0])
                
        except FileNotFoundError:
            raise FileNotFoundError(f"HDF file not found: {hdf_path}")
        except Exception as e:
            raise RuntimeError(f"Error reading unsteady attributes: {str(e)}")
        
    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_unsteady_summary(hdf_path: Path) -> pd.DataFrame:
        """
        Get results unsteady summary attributes from a HEC-RAS HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.
            ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
            pd.DataFrame: A DataFrame containing the decoded results unsteady summary attributes.

        Raises:
            FileNotFoundError: If the specified HDF file is not found.
            KeyError: If the "Results/Unsteady/Summary" group is not found in the HDF file.
        """
        try:           
            with h5py.File(hdf_path, 'r') as hdf_file:
                if "Results/Unsteady/Summary" not in hdf_file:
                    raise KeyError("Results/Unsteady/Summary group not found in the HDF file.")
                
                # Create dictionary from attributes and decode byte strings
                attrs_dict = {}
                for key, value in dict(hdf_file["Results/Unsteady/Summary"].attrs).items():
                    if isinstance(value, bytes):
                        attrs_dict[key] = value.decode('utf-8')
                    else:
                        attrs_dict[key] = value
                
                # Create DataFrame with a single row index
                return pd.DataFrame(attrs_dict, index=[0])
                
        except FileNotFoundError:
            raise FileNotFoundError(f"HDF file not found: {hdf_path}")
        except Exception as e:
            raise RuntimeError(f"Error reading unsteady summary attributes: {str(e)}")
        
    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_volume_accounting(hdf_path: Path) -> Optional[pd.DataFrame]:
        """
        Get volume accounting attributes from a HEC-RAS HDF plan file.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.
            ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
            Optional[pd.DataFrame]: DataFrame containing the decoded volume accounting attributes,
                                  or None if the group is not found.

        Raises:
            FileNotFoundError: If the specified HDF file is not found.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                if "Results/Unsteady/Summary/Volume Accounting" not in hdf_file:
                    return None
                
                # Get attributes and decode byte strings
                attrs_dict = {}
                for key, value in dict(hdf_file["Results/Unsteady/Summary/Volume Accounting"].attrs).items():
                    if isinstance(value, bytes):
                        attrs_dict[key] = value.decode('utf-8')
                    else:
                        attrs_dict[key] = value
                
                return pd.DataFrame(attrs_dict, index=[0])
                
        except FileNotFoundError:
            raise FileNotFoundError(f"HDF file not found: {hdf_path}")
        except Exception as e:
            raise RuntimeError(f"Error reading volume accounting attributes: {str(e)}")

    @staticmethod
    @standardize_input(file_type='plan_hdf')
    def get_runtime_data(hdf_path: Path) -> Optional[pd.DataFrame]:
        """
        Extract detailed runtime and computational performance metrics from HDF file.

        Args:
            hdf_path (Path): Path to HEC-RAS plan HDF file
            ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
            Optional[pd.DataFrame]: DataFrame containing runtime statistics or None if data cannot be extracted

        Notes:
            - Times are reported in multiple units (ms, s, hours)
            - Compute speeds are calculated as simulation-time/compute-time ratios
            - Process times include: geometry, preprocessing, event conditions, 
              and unsteady flow computations
        """
        try:
            if hdf_path is None:
                logger.error(f"Could not find HDF file for input")
                return None

            with h5py.File(hdf_path, 'r') as hdf_file:
                logger.info(f"Extracting Plan Information from: {Path(hdf_file.filename).name}")
                plan_info = hdf_file.get('/Plan Data/Plan Information')
                if plan_info is None:
                    logger.warning("Group '/Plan Data/Plan Information' not found.")
                    return None

                # Extract plan information
                plan_name = HdfUtils.convert_ras_string(plan_info.attrs.get('Plan Name', 'Unknown'))
                start_time_str = HdfUtils.convert_ras_string(plan_info.attrs.get('Simulation Start Time', 'Unknown'))
                end_time_str = HdfUtils.convert_ras_string(plan_info.attrs.get('Simulation End Time', 'Unknown'))

                try:
                    # Check if times are already datetime objects
                    if isinstance(start_time_str, datetime):
                        start_time = start_time_str
                    else:
                        start_time = datetime.strptime(start_time_str, "%d%b%Y %H:%M:%S")
                        
                    if isinstance(end_time_str, datetime):
                        end_time = end_time_str
                    else:
                        end_time = datetime.strptime(end_time_str, "%d%b%Y %H:%M:%S")
                        
                    simulation_duration = end_time - start_time
                    simulation_hours = simulation_duration.total_seconds() / 3600
                except ValueError as e:
                    logger.error(f"Error parsing simulation times: {e}")
                    return None

                logger.info(f"Plan Name: {plan_name}")
                logger.info(f"Simulation Duration (hours): {simulation_hours}")

                # Extract compute processes data
                compute_processes = hdf_file.get('/Results/Summary/Compute Processes')
                if compute_processes is None:
                    logger.warning("Dataset '/Results/Summary/Compute Processes' not found.")
                    return None

                # Process compute times
                process_names = [HdfUtils.convert_ras_string(name) for name in compute_processes['Process'][:]]
                filenames = [HdfUtils.convert_ras_string(filename) for filename in compute_processes['Filename'][:]]
                completion_times = compute_processes['Compute Time (ms)'][:]

                compute_processes_df = pd.DataFrame({
                    'Process': process_names,
                    'Filename': filenames,
                    'Compute Time (ms)': completion_times,
                    'Compute Time (s)': completion_times / 1000,
                    'Compute Time (hours)': completion_times / (1000 * 3600)
                })

                # Create summary DataFrame
                compute_processes_summary = {
                    'Plan Name': [plan_name],
                    'File Name': [Path(hdf_file.filename).name],
                    'Simulation Start Time': [start_time_str],
                    'Simulation End Time': [end_time_str],
                    'Simulation Duration (s)': [simulation_duration.total_seconds()],
                    'Simulation Time (hr)': [simulation_hours]
                }

                # Add process-specific times
                process_types = {
                    'Completing Geometry': 'Completing Geometry (hr)',
                    'Preprocessing Geometry': 'Preprocessing Geometry (hr)',
                    'Completing Event Conditions': 'Completing Event Conditions (hr)',
                    'Unsteady Flow Computations': 'Unsteady Flow Computations (hr)'
                }

                for process, column in process_types.items():
                    time_value = compute_processes_df[
                        compute_processes_df['Process'] == process
                    ]['Compute Time (hours)'].values[0] if process in process_names else 'N/A'
                    compute_processes_summary[column] = [time_value]

                # Add total process time
                total_time = compute_processes_df['Compute Time (hours)'].sum()
                compute_processes_summary['Complete Process (hr)'] = [total_time]

                # Calculate speeds
                if compute_processes_summary['Unsteady Flow Computations (hr)'][0] != 'N/A':
                    compute_processes_summary['Unsteady Flow Speed (hr/hr)'] = [
                        simulation_hours / compute_processes_summary['Unsteady Flow Computations (hr)'][0]
                    ]
                else:
                    compute_processes_summary['Unsteady Flow Speed (hr/hr)'] = ['N/A']

                compute_processes_summary['Complete Process Speed (hr/hr)'] = [
                    simulation_hours / total_time
                ]

                return pd.DataFrame(compute_processes_summary)

        except Exception as e:
            logger.error(f"Error in get_runtime_data: {str(e)}")
            return None

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_reference_timeseries(hdf_path: Path, reftype: str) -> pd.DataFrame:
        """
        Get reference line or point timeseries output from HDF file.

        Args:
            hdf_path (Path): Path to HEC-RAS plan HDF file
            reftype (str): Type of reference data ('lines' or 'points')
            ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
            pd.DataFrame: DataFrame containing reference timeseries data
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                base_path = "Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series"
                ref_path = f"{base_path}/Reference {reftype.capitalize()}"
                
                if ref_path not in hdf_file:
                    logger.warning(f"Reference {reftype} data not found in HDF file")
                    return pd.DataFrame()

                ref_group = hdf_file[ref_path]
                time_data = hdf_file[f"{base_path}/Time"][:]
                
                dfs = []
                for ref_name in ref_group.keys():
                    ref_data = ref_group[ref_name][:]
                    df = pd.DataFrame(ref_data, columns=[ref_name])
                    df['Time'] = time_data
                    dfs.append(df)

                if not dfs:
                    return pd.DataFrame()

                return pd.concat(dfs, axis=1)

        except Exception as e:
            logger.error(f"Error reading reference {reftype} timeseries: {str(e)}")
            return pd.DataFrame()

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_reference_summary(hdf_path: Path, reftype: str) -> pd.DataFrame:
        """
        Get reference line or point summary output from HDF file.

        Args:
            hdf_path (Path): Path to HEC-RAS plan HDF file
            reftype (str): Type of reference data ('lines' or 'points')
            ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
            pd.DataFrame: DataFrame containing reference summary data
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                base_path = "Results/Unsteady/Output/Output Blocks/Base Output/Summary Output"
                ref_path = f"{base_path}/Reference {reftype.capitalize()}"
                
                if ref_path not in hdf_file:
                    logger.warning(f"Reference {reftype} summary data not found in HDF file")
                    return pd.DataFrame()

                ref_group = hdf_file[ref_path]
                dfs = []
                
                for ref_name in ref_group.keys():
                    ref_data = ref_group[ref_name][:]
                    if ref_data.ndim == 2:
                        df = pd.DataFrame(ref_data.T, columns=['Value', 'Time'])
                    else:
                        df = pd.DataFrame({'Value': ref_data})
                    df['Reference'] = ref_name
                    dfs.append(df)

                if not dfs:
                    return pd.DataFrame()

                return pd.concat(dfs, ignore_index=True)

        except Exception as e:
            logger.error(f"Error reading reference {reftype} summary: {str(e)}")
            return pd.DataFrame()

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_compute_messages(hdf_path: Path) -> str:
        """
        Extract compute messages from a HEC-RAS plan HDF file.
        
        This method retrieves the computation log messages stored in the HDF file,
        which include timing information, computation tasks, and performance metrics.
        If the output exceeds 10,000 tokens, it will be truncated with the last 50 lines
        preserved.

        Args:
            hdf_path (Path): Path to the HEC-RAS plan HDF file.

        Returns:
            str: Formatted compute messages string. Returns an error message if
                 compute messages are not found or cannot be read.

        Raises:
            FileNotFoundError: If the specified HDF file is not found.
            RuntimeError: If there's an error reading the compute messages.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                compute_messages_path = '/Results/Summary/Compute Messages (text)'
                
                if compute_messages_path not in hdf_file:
                    return "Compute messages not found. The simulation may not have completed or results were not saved properly."
                
                # Extract the compute messages
                compute_messages_dataset = hdf_file[compute_messages_path]
                
                # Handle different data types
                if isinstance(compute_messages_dataset, h5py.Dataset):
                    data = compute_messages_dataset[()]
                    
                    # Convert to string based on data type
                    if isinstance(data, bytes):
                        messages_text = data.decode('utf-8')
                    elif isinstance(data, np.ndarray):
                        if data.dtype.kind == 'S':  # String array
                            # Join array of strings
                            messages_text = '\n'.join([item.decode('utf-8') if isinstance(item, bytes) else str(item) 
                                                      for item in data])
                        else:
                            messages_text = str(data)
                    else:
                        messages_text = str(data)
                else:
                    return f"Unexpected data type for compute messages: {type(compute_messages_dataset)}"
                
                # Format the output
                formatted_output = HdfResultsPlan._format_compute_messages(messages_text, str(hdf_path))
                
                # Check token count and truncate if necessary
                # Rough approximation: 1 token ≈ 4 characters
                max_chars = 10000 * 4  # 40,000 characters for 10k tokens
                
                if len(formatted_output) > max_chars:
                    # Truncate but preserve last 50 lines
                    lines = formatted_output.split('\n')
                    last_50_lines = lines[-50:] if len(lines) > 50 else lines
                    
                    # Find how many characters we can include from the beginning
                    last_50_text = '\n'.join(last_50_lines)
                    truncation_notice = "\n\n[OUTPUT TRUNCATED: Response exceeded 10,000 tokens. Showing beginning and last 50 lines.]\n\n"
                    
                    available_chars = max_chars - len(last_50_text) - len(truncation_notice)
                    truncated_beginning = formatted_output[:available_chars]
                    
                    # Find last complete line in truncated beginning
                    last_newline = truncated_beginning.rfind('\n')
                    if last_newline > 0:
                        truncated_beginning = truncated_beginning[:last_newline]
                    
                    formatted_output = truncated_beginning + truncation_notice + last_50_text
                
                return formatted_output
                
        except FileNotFoundError:
            raise FileNotFoundError(f"HDF file not found: {hdf_path}")
        except Exception as e:
            logger.error(f"Error reading compute messages: {str(e)}")
            raise RuntimeError(f"Error reading compute messages: {str(e)}")

    @staticmethod
    def _format_compute_messages(messages_text: str, hdf_file_path: str) -> str:
        """
        Format compute messages for better readability.
        
        Args:
            messages_text (str): Raw compute messages text
            hdf_file_path (str): Path to the HDF file for reference
            
        Returns:
            str: Formatted compute messages
        """
        lines = messages_text.split('\r\n') if '\r\n' in messages_text else messages_text.split('\n')
        
        formatted_parts = [
            f"Compute Messages from: {Path(hdf_file_path).name}",
            "=" * 80,
            ""
        ]
        
        # Track sections for organized output
        current_section = None
        computation_tasks = []
        computation_speeds = []
        general_messages = []
        
        for line in lines:
            line = line.strip()
            if not line:
                continue
                
            # Categorize messages
            if 'Computation Task' in line and '\t' in line:
                computation_tasks.append(line)
            elif 'Computation Speed' in line and '\t' in line:
                computation_speeds.append(line)
            else:
                general_messages.append(line)
        
        # Add general messages first
        if general_messages:
            formatted_parts.append("General Messages:")
            formatted_parts.append("-" * 40)
            for msg in general_messages:
                if ':' in msg and not msg.startswith('http'):
                    key, value = msg.split(':', 1)
                    formatted_parts.append(f"{key.strip():40} : {value.strip()}")
                else:
                    formatted_parts.append(msg)
            formatted_parts.append("")
        
        # Add computation tasks
        if computation_tasks:
            formatted_parts.append("Computation Tasks:")
            formatted_parts.append("-" * 60)
            formatted_parts.append(f"{'Task':<40} {'Time':<20}")
            formatted_parts.append("-" * 60)
            for task_line in computation_tasks:
                parts = task_line.split('\t')
                if len(parts) >= 2:
                    task = parts[0].replace('Computation Task', '').strip()
                    time = parts[1].strip()
                    formatted_parts.append(f"{task:<40} {time:<20}")
            formatted_parts.append("")
        
        # Add computation speeds
        if computation_speeds:
            formatted_parts.append("Computation Speed:")
            formatted_parts.append("-" * 60)
            formatted_parts.append(f"{'Task':<40} {'Simulation/Runtime':<20}")
            formatted_parts.append("-" * 60)
            for speed_line in computation_speeds:
                parts = speed_line.split('\t')
                if len(parts) >= 2:
                    task = parts[0].replace('Computation Speed', '').strip()
                    speed = parts[1].strip()
                    formatted_parts.append(f"{task:<40} {speed:<20}")
            formatted_parts.append("")
        
        return '\n'.join(formatted_parts)
==================================================

File: c:\GH\ras-commander-mcp-main\ras-commander reference files\HdfResultsXsec.py
==================================================
"""
Class: HdfResultsXsec

Contains methods for extracting 1D results data from HDF files. 
This includes cross section timeseries, structures and reference line/point timeseries as these are all 1D elements.

-----

All of the methods in this class are static and are designed to be used without instantiation.

List of Functions in HdfResultsXsec:
- get_xsec_timeseries(): Extract cross-section timeseries data including water surface, velocity, and flow
- get_ref_lines_timeseries(): Get timeseries output for reference lines
- get_ref_points_timeseries(): Get timeseries output for reference points

TO BE IMPLEMENTED: 
DSS Hydrograph Extraction for 1D and 2D Structures. 

Planned functions:
- get_bridge_timeseries(): Extract timeseries data for bridge structures
- get_inline_structures_timeseries(): Extract timeseries data for inline structures

Notes:
- All functions use the get_ prefix to indicate they return data
- Results data functions use results_ prefix to indicate they handle results data
- All functions include proper error handling and logging
- Functions return xarray Datasets for efficient handling of multi-dimensional data
"""

from pathlib import Path
from typing import Union, Optional, List, Dict, Tuple

import h5py
import numpy as np
import pandas as pd
import xarray as xr

from .HdfBase import HdfBase
from .HdfUtils import HdfUtils
from .Decorators import standardize_input, log_call
from .LoggingConfig import get_logger

logger = get_logger(__name__)

class HdfResultsXsec:
    """
    A static class for extracting and processing 1D results data from HEC-RAS HDF files.

    This class provides methods to extract and process unsteady flow simulation results
    for cross-sections, reference lines, and reference points. All methods are static
    and designed to be used without class instantiation.

    The class handles:
    - Cross-section timeseries (water surface, velocity, flow)
    - Reference line timeseries
    - Reference point timeseries

    Dependencies:
        - HdfBase: Core HDF file operations
        - HdfUtils: Utility functions for HDF processing
    """


# Tested functions from AWS webinar where the code was developed
# Need to add examples


    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_xsec_timeseries(hdf_path: Path) -> xr.Dataset:
        """
        Extract Water Surface, Velocity Total, Velocity Channel, Flow Lateral, and Flow data from HEC-RAS HDF file.
        Includes Cross Section Only and Cross Section Attributes as coordinates in the xarray.Dataset.
        Also calculates maximum values for key parameters.

        Parameters:
        -----------
        hdf_path : Path
            Path to the HEC-RAS results HDF file

        Returns:
        --------
        xr.Dataset
            Xarray Dataset containing the extracted cross-section results with appropriate coordinates and attributes.
            Includes maximum values for Water Surface, Flow, Channel Velocity, Total Velocity, and Lateral Flow.
        """
        try:
            with h5py.File(hdf_path, 'r') as hdf_file:
                # Define base paths
                base_output_path = "/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Cross Sections/"
                time_stamp_path = "/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Time Date Stamp (ms)"
                
                # Extract Cross Section Attributes
                attrs_dataset = hdf_file[f"{base_output_path}Cross Section Attributes"][:]
                rivers = [attr['River'].decode('utf-8').strip() for attr in attrs_dataset]
                reaches = [attr['Reach'].decode('utf-8').strip() for attr in attrs_dataset]
                stations = [attr['Station'].decode('utf-8').strip() for attr in attrs_dataset]
                names = [attr['Name'].decode('utf-8').strip() for attr in attrs_dataset]
                
                # Extract Cross Section Only (Unique Names)
                cross_section_only_dataset = hdf_file[f"{base_output_path}Cross Section Only"][:]
                cross_section_names = [cs.decode('utf-8').strip() for cs in cross_section_only_dataset]
                
                # Extract Time Stamps and convert to datetime
                time_stamps = hdf_file[time_stamp_path][:]
                if any(isinstance(ts, bytes) for ts in time_stamps):
                    time_stamps = [ts.decode('utf-8') for ts in time_stamps]
                # Convert RAS format timestamps to datetime
                times = pd.to_datetime(time_stamps, format='%d%b%Y %H:%M:%S:%f')
                
                # Extract Required Datasets
                water_surface = hdf_file[f"{base_output_path}Water Surface"][:]
                velocity_total = hdf_file[f"{base_output_path}Velocity Total"][:]
                velocity_channel = hdf_file[f"{base_output_path}Velocity Channel"][:]
                flow_lateral = hdf_file[f"{base_output_path}Flow Lateral"][:]
                flow = hdf_file[f"{base_output_path}Flow"][:]
                
                # Calculate maximum values along time axis
                max_water_surface = np.max(water_surface, axis=0)
                max_flow = np.max(flow, axis=0)
                max_velocity_channel = np.max(velocity_channel, axis=0)
                max_velocity_total = np.max(velocity_total, axis=0)
                max_flow_lateral = np.max(flow_lateral, axis=0)
                
                # Create Xarray Dataset
                ds = xr.Dataset(
                    {
                        'Water_Surface': (['time', 'cross_section'], water_surface),
                        'Velocity_Total': (['time', 'cross_section'], velocity_total),
                        'Velocity_Channel': (['time', 'cross_section'], velocity_channel),
                        'Flow_Lateral': (['time', 'cross_section'], flow_lateral),
                        'Flow': (['time', 'cross_section'], flow),
                    },
                    coords={
                        'time': times,
                        'cross_section': cross_section_names,
                        'River': ('cross_section', rivers),
                        'Reach': ('cross_section', reaches),
                        'Station': ('cross_section', stations),
                        'Name': ('cross_section', names),
                        'Maximum_Water_Surface': ('cross_section', max_water_surface),
                        'Maximum_Flow': ('cross_section', max_flow),
                        'Maximum_Channel_Velocity': ('cross_section', max_velocity_channel),
                        'Maximum_Velocity_Total': ('cross_section', max_velocity_total),
                        'Maximum_Flow_Lateral': ('cross_section', max_flow_lateral)
                    },
                    attrs={
                        'description': 'Cross-section results extracted from HEC-RAS HDF file',
                        'source_file': str(hdf_path)
                    }
                )
                
                return ds

        except KeyError as e:
            logger.error(f"Required dataset not found in HDF file: {e}")
            raise
        except Exception as e:
            logger.error(f"Error extracting cross section results: {e}")
            raise



    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_ref_lines_timeseries(hdf_path: Path) -> xr.Dataset:
        """
        Extract timeseries output data for reference lines from HEC-RAS HDF file.

        Parameters:
        -----------
        hdf_path : Path
            Path to the HEC-RAS results HDF file

        Returns:
        --------
        xr.Dataset
            Dataset containing flow, velocity, and water surface data for reference lines.
            Returns empty dataset if reference line data not found.

        Raises:
        -------
        FileNotFoundError
            If the specified HDF file is not found
        KeyError
            If required datasets are missing from the HDF file
        """
        return HdfResultsXsec._reference_timeseries_output(hdf_path, reftype="lines")

    @staticmethod
    @log_call
    @standardize_input(file_type='plan_hdf')
    def get_ref_points_timeseries(hdf_path: Path) -> xr.Dataset:
        """
        Extract timeseries output data for reference points from HEC-RAS HDF file.

        This method extracts flow, velocity, and water surface elevation data for all
        reference points defined in the model. Reference points are user-defined locations
        where detailed output is desired.

        Parameters:
        -----------
        hdf_path : Path
            Path to the HEC-RAS results HDF file

        Returns:
        --------
        xr.Dataset
            Dataset containing the following variables for each reference point:
            - Flow [cfs or m³/s]
            - Velocity [ft/s or m/s]
            - Water Surface [ft or m]
            
            The dataset includes coordinates:
            - time: Simulation timesteps
            - refpt_id: Unique identifier for each reference point
            - refpt_name: Name of each reference point
            - mesh_name: Associated 2D mesh area name
            
            Returns empty dataset if reference point data not found.

        Raises:
        -------
        FileNotFoundError
            If the specified HDF file is not found
        KeyError
            If required datasets are missing from the HDF file

        Examples:
        --------
        >>> ds = HdfResultsXsec.get_ref_points_timeseries("path/to/plan.hdf")
        >>> # Get water surface timeseries for first reference point
        >>> ws = ds['Water Surface'].isel(refpt_id=0)
        >>> # Get all data for a specific reference point by name
        >>> point_data = ds.sel(refpt_name='Point1')
        """
        return HdfResultsXsec._reference_timeseries_output(hdf_path, reftype="points")
    

    @staticmethod
    def _reference_timeseries_output(hdf_file: h5py.File, reftype: str = "lines") -> xr.Dataset:
        """
        Internal method to return timeseries output data for reference lines or points from a HEC-RAS HDF plan file.

        Parameters
        ----------
        hdf_file : h5py.File
            Open HDF file object.
        reftype : str, optional
            The type of reference data to retrieve. Must be either "lines" or "points".
            (default: "lines")

        Returns
        -------
        xr.Dataset
            An xarray Dataset with reference line or point timeseries data.
            Returns an empty Dataset if the reference output data is not found.

        Raises
        ------
        ValueError
            If reftype is not "lines" or "points".
        """
        if reftype == "lines":
            output_path = "Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Reference Lines"
            abbrev = "refln"
        elif reftype == "points":
            output_path = "Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Reference Points"
            abbrev = "refpt"
        else:
            raise ValueError('reftype must be either "lines" or "points".')

        try:
            reference_group = hdf_file[output_path]
        except KeyError:
            logger.error(f"Could not find HDF group at path '{output_path}'. "
                         f"The Plan HDF file may not contain reference {reftype[:-1]} output data.")
            return xr.Dataset()

        reference_names = reference_group["Name"][:]
        names = []
        mesh_areas = []
        for s in reference_names:
            name, mesh_area = s.decode("utf-8").split("|")
            names.append(name)
            mesh_areas.append(mesh_area)

        times = HdfBase.get_unsteady_timestamps(hdf_file)

        das = {}
        for var in ["Flow", "Velocity", "Water Surface"]:
            group = reference_group.get(var)
            if group is None:
                continue
            values = group[:]
            units = group.attrs["Units"].decode("utf-8")
            da = xr.DataArray(
                values,
                name=var,
                dims=["time", f"{abbrev}_id"],
                coords={
                    "time": times,
                    f"{abbrev}_id": range(values.shape[1]),
                    f"{abbrev}_name": (f"{abbrev}_id", names),
                    "mesh_name": (f"{abbrev}_id", mesh_areas),
                },
                attrs={"units": units, "hdf_path": f"{output_path}/{var}"},
            )
            das[var] = da
        return xr.Dataset(das)

==================================================

File: c:\GH\ras-commander-mcp-main\ras-commander reference files\RasPrj.py
==================================================
"""
RasPrj.py - Manages HEC-RAS projects within the ras-commander library

This module provides a class for managing HEC-RAS projects.

Classes:
    RasPrj: A class for managing HEC-RAS projects.

Functions:
    init_ras_project: Initialize a RAS project.
    get_ras_exe: Determine the HEC-RAS executable path based on the input.

DEVELOPER NOTE:
This class is used to initialize a RAS project and is used in conjunction with the RasCmdr class to manage the execution of RAS plans.
By default, the RasPrj class is initialized with the global 'ras' object.
However, you can create multiple RasPrj instances to manage multiple projects.
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.

This module is part of the ras-commander library and uses a centralized logging configuration.

Logging Configuration:
- The logging is set up in the logging_config.py file.
- A @log_call decorator is available to automatically log function calls.
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
- Logs are written to both console and a rotating file handler.
- The default log file is 'ras_commander.log' in the 'logs' directory.
- The default log level is INFO.

To use logging in this module:
1. Use the @log_call decorator for automatic function call logging.
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).


Example:
    @log_call
    def my_function():
        
        logger.debug("Additional debug information")
        # Function logic here
        
-----

All of the methods in this class are class methods and are designed to be used with instances of the class.

List of Functions in RasPrj:    
- initialize()
- _load_project_data()
- _get_geom_file_for_plan()
- _parse_plan_file()
- _parse_unsteady_file()
- _get_prj_entries()
- _parse_boundary_condition()
- is_initialized (property)
- check_initialized()
- find_ras_prj()
- get_project_name()
- get_prj_entries()
- get_plan_entries()
- get_flow_entries()
- get_unsteady_entries()
- get_geom_entries()
- get_hdf_entries()
- print_data()
- get_plan_value()
- get_boundary_conditions()
        
Functions in RasPrj that are not part of the class:        
- init_ras_project()
- get_ras_exe()

        
        
        
"""
import os
import re
from pathlib import Path
import pandas as pd
from typing import Union, Any, List, Dict, Tuple
import logging
from ras_commander.LoggingConfig import get_logger
from ras_commander.Decorators import log_call

logger = get_logger(__name__)

def read_file_with_fallback_encoding(file_path, encodings=['utf-8', 'latin1', 'cp1252', 'iso-8859-1']):
    """
    Attempt to read a file using multiple encodings.
    
    Args:
        file_path (str or Path): Path to the file to read
        encodings (list): List of encodings to try, in order of preference
    
    Returns:
        tuple: (content, encoding) or (None, None) if all encodings fail
    """
    for encoding in encodings:
        try:
            with open(file_path, 'r', encoding=encoding) as file:
                content = file.read()
                return content, encoding
        except UnicodeDecodeError:
            continue
        except Exception as e:
            logger.error(f"Error reading file {file_path} with {encoding} encoding: {e}")
            continue
    
    logger.error(f"Failed to read file {file_path} with any of the attempted encodings: {encodings}")
    return None, None

class RasPrj:
    
    def __init__(self):
        self.initialized = False
        self.boundaries_df = None  # New attribute to store boundary conditions
        self.suppress_logging = False  # Add suppress_logging as instance variable

    @log_call
    def initialize(self, project_folder, ras_exe_path, suppress_logging=True):
        """
        Initialize a RasPrj instance with project folder and RAS executable path.

        IMPORTANT: External users should use init_ras_project() function instead of this method.
        This method is intended for internal use only.

        Args:
            project_folder (str or Path): Path to the HEC-RAS project folder.
            ras_exe_path (str or Path): Path to the HEC-RAS executable.
            suppress_logging (bool, default=True): If True, suppresses initialization logging messages.

        Raises:
            ValueError: If no HEC-RAS project file is found in the specified folder.

        Note:
            This method sets up the RasPrj instance by:
            1. Finding the project file (.prj)
            2. Loading project data (plans, geometries, flows)
            3. Extracting boundary conditions
            4. Setting the initialization flag
            5. Loading RASMapper data (.rasmap)
        """
        self.suppress_logging = suppress_logging  # Store suppress_logging state
        self.project_folder = Path(project_folder)
        self.prj_file = self.find_ras_prj(self.project_folder)
        if self.prj_file is None:
            logger.error(f"No HEC-RAS project file found in {self.project_folder}")
            raise ValueError(f"No HEC-RAS project file found in {self.project_folder}. Please check the path and try again.")
        self.project_name = Path(self.prj_file).stem
        self.ras_exe_path = ras_exe_path
        
        # Set initialized to True before loading project data
        self.initialized = True
        
        # Now load the project data
        self._load_project_data()
        self.boundaries_df = self.get_boundary_conditions()
        
        # Load RASMapper data if available
        try:
            # Import here to avoid circular imports
            from .RasMap import RasMap
            self.rasmap_df = RasMap.initialize_rasmap_df(self)
        except ImportError:
            logger.warning("RasMap module not available. RASMapper data will not be loaded.")
            self.rasmap_df = pd.DataFrame(columns=['projection_path', 'profile_lines_path', 'soil_layer_path', 
                                                'infiltration_hdf_path', 'landcover_hdf_path', 'terrain_hdf_path', 
                                                'current_settings'])
        except Exception as e:
            logger.error(f"Error initializing RASMapper data: {e}")
            self.rasmap_df = pd.DataFrame(columns=['projection_path', 'profile_lines_path', 'soil_layer_path', 
                                                'infiltration_hdf_path', 'landcover_hdf_path', 'terrain_hdf_path', 
                                                'current_settings'])

        if not suppress_logging:
            logger.info(f"Initialization complete for project: {self.project_name}")
            logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
                         f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
                         f"Boundary conditions: {len(self.boundaries_df)}")
            logger.info(f"Geometry HDF files found: {self.plan_df['Geom_File'].notna().sum()}")
            logger.info(f"RASMapper data loaded: {not self.rasmap_df.empty}")

    @log_call
    def _load_project_data(self):
        """
        Load project data from the HEC-RAS project file.

        This internal method:
        1. Initializes DataFrames for plan, flow, unsteady, and geometry entries
        2. Ensures all required columns are present with appropriate default values
        3. Sets file paths for all components (geometries, flows, plans)

        Raises:
            Exception: If there's an error loading or processing project data.
        """
        try:
            # Load data frames
            self.unsteady_df = self._get_prj_entries('Unsteady')
            self.plan_df = self._get_prj_entries('Plan')
            self.flow_df = self._get_prj_entries('Flow')
            self.geom_df = self.get_geom_entries()
            
            # Ensure required columns exist
            self._ensure_required_columns()
            
            # Set paths for geometry and flow files
            self._set_file_paths()
            
            # Make sure all plan paths are properly set
            self._set_plan_paths()
            
        except Exception as e:
            logger.error(f"Error loading project data: {e}")
            raise

    def _ensure_required_columns(self):
        """Ensure all required columns exist in plan_df."""
        required_columns = [
            'plan_number', 'unsteady_number', 'geometry_number',
            'Geom File', 'Geom Path', 'Flow File', 'Flow Path', 'full_path'
        ]
        
        for col in required_columns:
            if col not in self.plan_df.columns:
                self.plan_df[col] = None
        
        if not self.plan_df['full_path'].any():
            self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
                lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
            )

    def _set_file_paths(self):
        """Set geometry and flow paths in plan_df."""
        for idx, row in self.plan_df.iterrows():
            try:
                self._set_geom_path(idx, row)
                self._set_flow_path(idx, row)
                
                if not self.suppress_logging:
                    logger.info(f"Plan {row['plan_number']} paths set up")
            except Exception as e:
                logger.error(f"Error processing plan file {row['plan_number']}: {e}")

    def _set_geom_path(self, idx: int, row: pd.Series):
        """Set geometry path for a plan entry."""
        if pd.notna(row['Geom File']):
            geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
            self.plan_df.at[idx, 'Geom Path'] = str(geom_path)

    def _set_flow_path(self, idx: int, row: pd.Series):
        """Set flow path for a plan entry."""
        if pd.notna(row['Flow File']):
            prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
            flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
            self.plan_df.at[idx, 'Flow Path'] = str(flow_path)

    def _set_plan_paths(self):
        """Set full path information for plan files and their associated geometry and flow files."""
        if self.plan_df.empty:
            logger.debug("Plan DataFrame is empty, no paths to set")
            return
        
        # Ensure full path is set for all plan entries
        if 'full_path' not in self.plan_df.columns or self.plan_df['full_path'].isna().any():
            self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
                lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
            )
        
        # Create the Geom Path and Flow Path columns if they don't exist
        if 'Geom Path' not in self.plan_df.columns:
            self.plan_df['Geom Path'] = None
        if 'Flow Path' not in self.plan_df.columns:
            self.plan_df['Flow Path'] = None
        
        # Update paths for each plan entry
        for idx, row in self.plan_df.iterrows():
            try:
                # Set geometry path if Geom File exists and Geom Path is missing or invalid
                if pd.notna(row['Geom File']):
                    geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
                    self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
                
                # Set flow path if Flow File exists and Flow Path is missing or invalid
                if pd.notna(row['Flow File']):
                    # Determine the prefix (u for unsteady, f for steady flow)
                    prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
                    flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
                    self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
                
                if not self.suppress_logging:
                    logger.debug(f"Plan {row['plan_number']} paths set up")
            except Exception as e:
                logger.error(f"Error setting paths for plan {row.get('plan_number', idx)}: {e}")

    def _get_geom_file_for_plan(self, plan_number):
        """
        Get the geometry file path for a given plan number.
        
        Args:
            plan_number (str): The plan number to find the geometry file for.
        
        Returns:
            str: The full path to the geometry HDF file, or None if not found.
        """
        plan_file_path = self.project_folder / f"{self.project_name}.p{plan_number}"
        content, encoding = read_file_with_fallback_encoding(plan_file_path)
        
        if content is None:
            return None
        
        try:
            for line in content.splitlines():
                if line.startswith("Geom File="):
                    geom_file = line.strip().split('=')[1]
                    geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
                    if geom_hdf_path.exists():
                        return str(geom_hdf_path)
                    else:
                        return None
        except Exception as e:
            logger.error(f"Error reading plan file for geometry: {e}")
        return None


    @staticmethod
    @log_call
    def get_plan_value(
        plan_number_or_path: Union[str, Path],
        key: str,
        ras_object=None
    ) -> Any:
        """
        Retrieve a specific value from a HEC-RAS plan file.

        Parameters:
        plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
        key (str): The key to retrieve from the plan file
        ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.

        Returns:
        Any: The value associated with the specified key

        Raises:
        ValueError: If the plan file is not found
        IOError: If there's an error reading the plan file
        """
        logger = get_logger(__name__)
        ras_obj = ras_object or ras
        ras_obj.check_initialized()

        # These must exactly match the keys in supported_plan_keys from _parse_plan_file
        valid_keys = {
            'Computation Interval',
            'DSS File',
            'Flow File',
            'Friction Slope Method',
            'Geom File',
            'Mapping Interval',
            'Plan Title',
            'Program Version',
            'Run HTab',
            'Run PostProcess',
            'Run Sediment',
            'Run UNet',
            'Run WQNet',
            'Short Identifier',
            'Simulation Date',
            'UNET D1 Cores',
            'UNET D2 Cores',
            'PS Cores',
            'UNET Use Existing IB Tables',
            'UNET 1D Methodology',
            'UNET D2 SolverType',
            'UNET D2 Name',
            'description'  # Special case for description block
        }

        if key not in valid_keys:
            logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(sorted(valid_keys))}")
            return None

        plan_file_path = Path(plan_number_or_path)
        if not plan_file_path.is_file():
            plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
            if not plan_file_path.exists():
                logger.error(f"Plan file not found: {plan_file_path}")
                raise ValueError(f"Plan file not found: {plan_file_path}")

        try:
            with open(plan_file_path, 'r') as file:
                content = file.read()
        except IOError as e:
            logger.error(f"Error reading plan file {plan_file_path}: {e}")
            raise

        if key == 'description':
            match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
            return match.group(1).strip() if match else None
        else:
            pattern = f"{key}=(.*)"
            match = re.search(pattern, content)
            if match:
                value = match.group(1).strip()
                # Convert core values to integers
                if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
                    try:
                        return int(value)
                    except ValueError:
                        logger.warning(f"Could not convert {key} value '{value}' to integer")
                        return None
                return value
            
            # Use DEBUG level for missing core values, ERROR for other missing keys
            if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
                logger.debug(f"Core setting '{key}' not found in plan file")
            else:
                logger.error(f"Key '{key}' not found in the plan file")
            return None

    def _parse_plan_file(self, plan_file_path):
        """
        Parse a plan file and extract critical information.
        
        Args:
            plan_file_path (Path): Path to the plan file.
        
        Returns:
            dict: Dictionary containing extracted plan information.
        """
        plan_info = {}
        content, encoding = read_file_with_fallback_encoding(plan_file_path)
        
        if content is None:
            logger.error(f"Could not read plan file {plan_file_path} with any supported encoding")
            return plan_info
        
        try:
            # Extract description
            description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
            if description_match:
                plan_info['description'] = description_match.group(1).strip()
            
            # BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
            
            # Extract other critical information
            supported_plan_keys = {
                'Computation Interval': r'Computation Interval=(.+)',
                'DSS File': r'DSS File=(.+)',
                'Flow File': r'Flow File=(.+)',
                'Friction Slope Method': r'Friction Slope Method=(.+)',
                'Geom File': r'Geom File=(.+)',
                'Mapping Interval': r'Mapping Interval=(.+)',
                'Plan Title': r'Plan Title=(.+)',
                'Program Version': r'Program Version=(.+)',
                'Run HTab': r'Run HTab=(.+)',
                'Run PostProcess': r'Run PostProcess=(.+)',
                'Run Sediment': r'Run Sediment=(.+)',
                'Run UNet': r'Run UNet=(.+)',
                'Run WQNet': r'Run WQNet=(.+)',
                'Short Identifier': r'Short Identifier=(.+)',
                'Simulation Date': r'Simulation Date=(.+)',
                'UNET D1 Cores': r'UNET D1 Cores=(.+)',
                'UNET D2 Cores': r'UNET D2 Cores=(.+)',
                'PS Cores': r'PS Cores=(.+)',
                'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
                'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
                'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
                'UNET D2 Name': r'UNET D2 Name=(.+)'
            }
            
            # END Exception to Style Guide
            
            # First, explicitly set None for core values
            core_keys = ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']
            for key in core_keys:
                plan_info[key] = None
            
            for key, pattern in supported_plan_keys.items():
                match = re.search(pattern, content)
                if match:
                    value = match.group(1).strip()
                    # Convert core values to integers if they exist
                    if key in core_keys and value:
                        try:
                            value = int(value)
                        except ValueError:
                            logger.warning(f"Could not convert {key} value '{value}' to integer in plan file {plan_file_path}")
                            value = None
                    plan_info[key] = value
                elif key in core_keys:
                    logger.debug(f"Core setting '{key}' not found in plan file {plan_file_path}")
            
            logger.debug(f"Parsed plan file: {plan_file_path} using {encoding} encoding")
        except Exception as e:
            logger.error(f"Error parsing plan file {plan_file_path}: {e}")
        
        return plan_info

    @log_call
    def _get_prj_entries(self, entry_type):
        """
        Extract entries of a specific type from the HEC-RAS project file.
        
        Args:
            entry_type (str): The type of entry to extract (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
        
        Returns:
            pd.DataFrame: A DataFrame containing the extracted entries.
        
        Raises:
            Exception: If there's an error reading or processing the project file.
        """
        entries = []
        pattern = re.compile(rf"{entry_type} File=(\w+)")

        try:
            with open(self.prj_file, 'r', encoding='utf-8') as file:
                for line in file:
                    match = pattern.match(line.strip())
                    if match:
                        file_name = match.group(1)
                        full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
                        entry_number = file_name[1:]
                        
                        entry = {
                            f'{entry_type.lower()}_number': entry_number,
                            'full_path': full_path
                        }
                        
                        # Handle Unsteady entries
                        if entry_type == 'Unsteady':
                            entry.update(self._process_unsteady_entry(entry_number, full_path))
                        else:
                            entry.update(self._process_default_entry())
                        
                        # Handle Plan entries
                        if entry_type == 'Plan':
                            entry.update(self._process_plan_entry(entry_number, full_path))
                        
                        entries.append(entry)
            
            df = pd.DataFrame(entries)
            return self._format_dataframe(df, entry_type)
        
        except Exception as e:
            logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
            raise

    def _process_unsteady_entry(self, entry_number: str, full_path: str) -> dict:
        """Process unsteady entry data."""
        entry = {'unsteady_number': entry_number}
        unsteady_info = self._parse_unsteady_file(Path(full_path))
        entry.update(unsteady_info)
        return entry

    def _process_default_entry(self) -> dict:
        """Process default entry data."""
        return {
            'unsteady_number': None,
            'geometry_number': None
        }

    def _process_plan_entry(self, entry_number: str, full_path: str) -> dict:
        """Process plan entry data."""
        entry = {}
        plan_info = self._parse_plan_file(Path(full_path))
        
        if plan_info:
            entry.update(self._process_flow_file(plan_info))
            entry.update(self._process_geom_file(plan_info))
            
            # Add remaining plan info
            for key, value in plan_info.items():
                if key not in ['Flow File', 'Geom File']:
                    entry[key] = value
            
            # Add HDF results path
            hdf_results_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
            entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
        
        return entry

    def _process_flow_file(self, plan_info: dict) -> dict:
        """Process flow file information from plan info."""
        flow_file = plan_info.get('Flow File')
        if flow_file and flow_file.startswith('u'):
            return {
                'unsteady_number': flow_file[1:],
                'Flow File': flow_file[1:]
            }
        return {
            'unsteady_number': None,
            'Flow File': flow_file[1:] if flow_file and flow_file.startswith('f') else None
        }

    def _process_geom_file(self, plan_info: dict) -> dict:
        """Process geometry file information from plan info."""
        geom_file = plan_info.get('Geom File')
        if geom_file and geom_file.startswith('g'):
            return {
                'geometry_number': geom_file[1:],
                'Geom File': geom_file[1:]
            }
        return {
            'geometry_number': None,
            'Geom File': None
        }

    def _parse_unsteady_file(self, unsteady_file_path):
        """
        Parse an unsteady flow file and extract critical information.
        
        Args:
            unsteady_file_path (Path): Path to the unsteady flow file.
        
        Returns:
            dict: Dictionary containing extracted unsteady flow information.
        """
        unsteady_info = {}
        content, encoding = read_file_with_fallback_encoding(unsteady_file_path)
        
        if content is None:
            return unsteady_info
        
        try:
            # BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
            
            supported_unsteady_keys = {
                'Flow Title': r'Flow Title=(.+)',
                'Program Version': r'Program Version=(.+)',
                'Use Restart': r'Use Restart=(.+)',
                'Precipitation Mode': r'Precipitation Mode=(.+)',
                'Wind Mode': r'Wind Mode=(.+)',
                'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
                'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
                'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
                'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
                'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
            }
            
            # END Exception to Style Guide
            
            for key, pattern in supported_unsteady_keys.items():
                match = re.search(pattern, content)
                if match:
                    unsteady_info[key] = match.group(1).strip()
        
        except Exception as e:
            logger.error(f"Error parsing unsteady file {unsteady_file_path}: {e}")
        
        return unsteady_info

    @property
    def is_initialized(self):
        """
        Check if the RasPrj instance has been initialized.

        Returns:
            bool: True if the instance has been initialized, False otherwise.
        """
        return self.initialized

    @log_call
    def check_initialized(self):
        """
        Ensure that the RasPrj instance has been initialized before operations.

        Raises:
            RuntimeError: If the project has not been initialized with init_ras_project().

        Note:
            This method is called by other methods to validate the project state before
            performing operations. Users typically don't need to call this directly.
        """
        if not self.initialized:
            raise RuntimeError("Project not initialized. Call init_ras_project() first.")

    @staticmethod
    @log_call
    def find_ras_prj(folder_path):
        """
        Find the appropriate HEC-RAS project file (.prj) in the given folder.
        
        This method uses several strategies to locate the correct project file:
        1. If only one .prj file exists, it is selected
        2. If multiple .prj files exist, it tries to match with .rasmap file names
        3. As a last resort, it scans files for "Proj Title=" content
        
        Args:
            folder_path (str or Path): Path to the folder containing HEC-RAS files.
        
        Returns:
            Path: The full path of the selected .prj file or None if no suitable file is found.
        
        Example:
            >>> project_file = RasPrj.find_ras_prj("/path/to/ras_project")
            >>> if project_file:
            ...     print(f"Found project file: {project_file}")
            ... else:
            ...     print("No project file found")
        """
        folder_path = Path(folder_path)
        prj_files = list(folder_path.glob("*.prj"))
        rasmap_files = list(folder_path.glob("*.rasmap"))
        if len(prj_files) == 1:
            return prj_files[0].resolve()
        if len(prj_files) > 1:
            if len(rasmap_files) == 1:
                base_filename = rasmap_files[0].stem
                prj_file = folder_path / f"{base_filename}.prj"
                if prj_file.exists():
                    return prj_file.resolve()
            for prj_file in prj_files:
                try:
                    with open(prj_file, 'r') as file:
                        content = file.read()
                        if "Proj Title=" in content:
                            return prj_file.resolve()
                except Exception:
                    continue
        return None


    @log_call
    def get_project_name(self):
        """
        Get the name of the HEC-RAS project (without file extension).

        Returns:
            str: The name of the project.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> project_name = ras.get_project_name()
            >>> print(f"Working with project: {project_name}")
        """
        self.check_initialized()
        return self.project_name

    @log_call
    def get_prj_entries(self, entry_type):
        """
        Get entries of a specific type from the HEC-RAS project.

        This method extracts files of the specified type from the project file,
        parses their content, and returns a structured DataFrame.

        Args:
            entry_type (str): The type of entry to retrieve ('Plan', 'Flow', 'Unsteady', or 'Geom').

        Returns:
            pd.DataFrame: A DataFrame containing the requested entries with appropriate columns.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> # Get all geometry files in the project
            >>> geom_entries = ras.get_prj_entries('Geom')
            >>> print(f"Project contains {len(geom_entries)} geometry files")
        
        Note:
            This is a generic method. For specific file types, use the dedicated methods:
            get_plan_entries(), get_flow_entries(), get_unsteady_entries(), get_geom_entries()
        """
        self.check_initialized()
        return self._get_prj_entries(entry_type)

    @log_call
    def get_plan_entries(self):
        """
        Get all plan entries from the HEC-RAS project.
        
        Returns a DataFrame containing all plan files (.p*) in the project
        with their associated properties, paths and settings.

        Returns:
            pd.DataFrame: A DataFrame with columns including 'plan_number', 'full_path',
                          'unsteady_number', 'geometry_number', etc.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> plan_entries = ras.get_plan_entries()
            >>> print(f"Project contains {len(plan_entries)} plan files")
            >>> # Display the first plan's properties
            >>> if not plan_entries.empty:
            ...     print(plan_entries.iloc[0])
        """
        self.check_initialized()
        return self._get_prj_entries('Plan')

    @log_call
    def get_flow_entries(self):
        """
        Get all flow entries from the HEC-RAS project.
        
        Returns a DataFrame containing all flow files (.f*) in the project
        with their associated properties and paths.

        Returns:
            pd.DataFrame: A DataFrame with columns including 'flow_number', 'full_path', etc.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> flow_entries = ras.get_flow_entries()
            >>> print(f"Project contains {len(flow_entries)} flow files")
            >>> # Display the first flow file's properties
            >>> if not flow_entries.empty:
            ...     print(flow_entries.iloc[0])
        """
        self.check_initialized()
        return self._get_prj_entries('Flow')

    @log_call
    def get_unsteady_entries(self):
        """
        Get all unsteady flow entries from the HEC-RAS project.
        
        Returns a DataFrame containing all unsteady flow files (.u*) in the project
        with their associated properties and paths.

        Returns:
            pd.DataFrame: A DataFrame with columns including 'unsteady_number', 'full_path', etc.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> unsteady_entries = ras.get_unsteady_entries()
            >>> print(f"Project contains {len(unsteady_entries)} unsteady flow files")
            >>> # Display the first unsteady file's properties
            >>> if not unsteady_entries.empty:
            ...     print(unsteady_entries.iloc[0])
        """
        self.check_initialized()
        return self._get_prj_entries('Unsteady')

    @log_call
    def get_geom_entries(self):
        """
        Get all geometry entries from the HEC-RAS project.
        
        Returns a DataFrame containing all geometry files (.g*) in the project
        with their associated properties, paths and HDF links.

        Returns:
            pd.DataFrame: A DataFrame with columns including 'geom_number', 'full_path', 
                          'hdf_path', etc.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> geom_entries = ras.get_geom_entries()
            >>> print(f"Project contains {len(geom_entries)} geometry files")
            >>> # Display the first geometry file's properties
            >>> if not geom_entries.empty:
            ...     print(geom_entries.iloc[0])
        """
        self.check_initialized()
        geom_pattern = re.compile(r'Geom File=(\w+)')
        geom_entries = []

        try:
            with open(self.prj_file, 'r') as f:
                for line in f:
                    match = geom_pattern.search(line)
                    if match:
                        geom_entries.append(match.group(1))
        
            geom_df = pd.DataFrame({'geom_file': geom_entries})
            geom_df['geom_number'] = geom_df['geom_file'].str.extract(r'(\d+)$')
            geom_df['full_path'] = geom_df['geom_file'].apply(lambda x: str(self.project_folder / f"{self.project_name}.{x}"))
            geom_df['hdf_path'] = geom_df['full_path'] + ".hdf"
            
            if not self.suppress_logging:  # Only log if suppress_logging is False
                logger.info(f"Found {len(geom_df)} geometry entries")
            return geom_df
        except Exception as e:
            logger.error(f"Error reading geometry entries from project file: {e}")
            raise
    
    @log_call
    def get_hdf_entries(self):
        """
        Get all plan entries that have associated HDF results files.
        
        This method identifies which plans have been successfully computed
        and have HDF results available for further analysis.
        
        Returns:
            pd.DataFrame: A DataFrame containing plan entries with HDF results.
                          Returns an empty DataFrame if no results are found.
        
        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> hdf_entries = ras.get_hdf_entries()
            >>> if hdf_entries.empty:
            ...     print("No computed results found. Run simulations first.")
            ... else:
            ...     print(f"Found results for {len(hdf_entries)} plans")
        
        Note:
            This is useful for identifying which plans have been successfully computed
            and can be used for further results analysis.
        """
        self.check_initialized()
        
        hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
        
        if hdf_entries.empty:
            return pd.DataFrame(columns=self.plan_df.columns)
        
        return hdf_entries
    
    
    @log_call
    def print_data(self):
        """
        Print a comprehensive summary of all RAS Object data for this instance.
        
        This method outputs:
        - Project information (name, folder, file paths)
        - Summary of plans, flows, geometries, and unsteady files
        - HDF results availability
        - Boundary conditions
        
        Useful for debugging, validation, and exploring project structure.

        Raises:
            RuntimeError: If the project has not been initialized.
        
        Example:
            >>> ras.print_data()  # Displays complete project overview
        """
        self.check_initialized()
        logger.info(f"--- Data for {self.project_name} ---")
        logger.info(f"Project folder: {self.project_folder}")
        logger.info(f"PRJ file: {self.prj_file}")
        logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
        logger.info("Plan files:")
        logger.info(f"\n{self.plan_df}")
        logger.info("Flow files:")
        logger.info(f"\n{self.flow_df}")
        logger.info("Unsteady flow files:")
        logger.info(f"\n{self.unsteady_df}")
        logger.info("Geometry files:")
        logger.info(f"\n{self.geom_df}")
        logger.info("HDF entries:")
        logger.info(f"\n{self.get_hdf_entries()}")
        logger.info("Boundary conditions:")
        logger.info(f"\n{self.boundaries_df}")
        logger.info("----------------------------")

    @log_call
    def get_boundary_conditions(self) -> pd.DataFrame:
        """
        Extract boundary conditions from unsteady flow files into a structured DataFrame.

        This method:
        1. Parses all unsteady flow files to extract boundary condition information
        2. Creates a structured DataFrame with boundary locations, types and parameters
        3. Links boundary conditions to their respective unsteady flow files

        Supported boundary condition types include:
        - Flow Hydrograph
        - Stage Hydrograph
        - Normal Depth
        - Lateral Inflow Hydrograph
        - Uniform Lateral Inflow Hydrograph
        - Gate Opening

        Returns:
            pd.DataFrame: A DataFrame containing detailed boundary condition information.
                              Returns an empty DataFrame if no unsteady flow files are present.
        
        Example:
            >>> boundaries = ras.get_boundary_conditions()
            >>> if not boundaries.empty:
            ...     print(f"Found {len(boundaries)} boundary conditions")
            ...     # Show flow hydrographs only
            ...     flow_hydrographs = boundaries[boundaries['bc_type'] == 'Flow Hydrograph']
            ...     print(f"Project has {len(flow_hydrographs)} flow hydrographs")
        
        Note:
            To see unparsed boundary condition lines for debugging, set logging to DEBUG:
            import logging
            logging.getLogger().setLevel(logging.DEBUG)
        """
        boundary_data = []
        
        # Check if unsteady_df is empty
        if self.unsteady_df.empty:
            logger.info("No unsteady flow files found in the project.")
            return pd.DataFrame()  # Return an empty DataFrame
        
        for _, row in self.unsteady_df.iterrows():
            unsteady_file_path = row['full_path']
            unsteady_number = row['unsteady_number']
            
            try:
                with open(unsteady_file_path, 'r') as file:
                    content = file.read()
            except IOError as e:
                logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
                continue
                
            bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
            
            for i, block in enumerate(bc_blocks, 1):
                bc_info, unparsed_lines = self._parse_boundary_condition(block, unsteady_number, i)
                boundary_data.append(bc_info)
                
                if unparsed_lines:
                    logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
        
        if not boundary_data:
            logger.info("No boundary conditions found in unsteady flow files.")
            return pd.DataFrame()  # Return an empty DataFrame if no boundary conditions were found
        
        boundaries_df = pd.DataFrame(boundary_data)
        
        # Merge with unsteady_df to get relevant unsteady flow file information
        merged_df = pd.merge(boundaries_df, self.unsteady_df, 
                             left_on='unsteady_number', right_on='unsteady_number', how='left')
        
        return merged_df

    def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
        lines = block.split('\n')
        bc_info = {
            'unsteady_number': unsteady_number,
            'boundary_condition_number': bc_number
        }
        
        parsed_lines = set()
        
        # Parse Boundary Location
        boundary_location = lines[0].split('=')[1].strip()
        fields = [field.strip() for field in boundary_location.split(',')]
        bc_info.update({
            'river_reach_name': fields[0] if len(fields) > 0 else '',
            'river_station': fields[1] if len(fields) > 1 else '',
            'storage_area_name': fields[2] if len(fields) > 2 else '',
            'pump_station_name': fields[3] if len(fields) > 3 else ''
        })
        parsed_lines.add(0)
        
        # Determine BC Type
        bc_types = {
            'Flow Hydrograph=': 'Flow Hydrograph',
            'Lateral Inflow Hydrograph=': 'Lateral Inflow Hydrograph',
            'Uniform Lateral Inflow Hydrograph=': 'Uniform Lateral Inflow Hydrograph',
            'Stage Hydrograph=': 'Stage Hydrograph',
            'Friction Slope=': 'Normal Depth',
            'Gate Name=': 'Gate Opening'
        }
        
        bc_info['bc_type'] = 'Unknown'
        bc_info['hydrograph_type'] = None
        for i, line in enumerate(lines[1:], 1):
            for key, bc_type in bc_types.items():
                if line.startswith(key):
                    bc_info['bc_type'] = bc_type
                    if 'Hydrograph' in bc_type:
                        bc_info['hydrograph_type'] = bc_type
                    parsed_lines.add(i)
                    break
            if bc_info['bc_type'] != 'Unknown':
                break
        
        # Parse other fields
        known_fields = ['Interval', 'DSS Path', 'Use DSS', 'Use Fixed Start Time', 'Fixed Start Date/Time',
                        'Is Critical Boundary', 'Critical Boundary Flow', 'DSS File']
        for i, line in enumerate(lines):
            if '=' in line:
                key, value = line.split('=', 1)
                key = key.strip()
                if key in known_fields:
                    bc_info[key] = value.strip()
                    parsed_lines.add(i)
        
        # Handle hydrograph values
        bc_info['hydrograph_num_values'] = 0
        if bc_info['hydrograph_type']:
            hydrograph_key = f"{bc_info['hydrograph_type']}="
            hydrograph_line = next((line for i, line in enumerate(lines) if line.startswith(hydrograph_key)), None)
            if hydrograph_line:
                hydrograph_index = lines.index(hydrograph_line)
                values_count = int(hydrograph_line.split('=')[1].strip())
                bc_info['hydrograph_num_values'] = values_count
                if values_count > 0:
                    values = ' '.join(lines[hydrograph_index + 1:]).split()[:values_count]
                    bc_info['hydrograph_values'] = values
                    parsed_lines.update(range(hydrograph_index, hydrograph_index + (values_count // 5) + 2))
        
        # Collect unparsed lines
        unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
        
        if unparsed_lines:
            logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
        
        return bc_info, unparsed_lines

    @log_call
    def _format_dataframe(self, df, entry_type):
        """
        Format the DataFrame according to the desired column structure.
        
        Args:
            df (pd.DataFrame): The DataFrame to format.
            entry_type (str): The type of entry (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
        
        Returns:
            pd.DataFrame: The formatted DataFrame.
        """
        if df.empty:
            return df
        
        if entry_type == 'Plan':
            # Set required column order
            first_cols = ['plan_number', 'unsteady_number', 'geometry_number']
            
            # Standard plan key columns in the exact order specified
            plan_key_cols = [
                'Plan Title', 'Program Version', 'Short Identifier', 'Simulation Date',
                'Std Step Tol', 'Computation Interval', 'Output Interval', 'Instantaneous Interval',
                'Mapping Interval', 'Run HTab', 'Run UNet', 'Run Sediment', 'Run PostProcess',
                'Run WQNet', 'Run RASMapper', 'UNET Use Existing IB Tables', 'HDF_Results_Path',
                'UNET 1D Methodology', 'Write IC File', 'Write IC File at Fixed DateTime',
                'IC Time', 'Write IC File Reoccurance', 'Write IC File at Sim End'
            ]
            
            # Additional convenience columns
            file_path_cols = ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']
            
            # Special columns that must be preserved
            special_cols = ['HDF_Results_Path']
            
            # Build the final column list
            all_cols = first_cols.copy()
            
            # Add plan key columns if they exist
            for col in plan_key_cols:
                if col in df.columns and col not in all_cols and col not in special_cols:
                    all_cols.append(col)
            
            # Add any remaining columns not explicitly specified
            other_cols = [col for col in df.columns if col not in all_cols + file_path_cols + special_cols + ['full_path']]
            all_cols.extend(other_cols)
            
            # Add HDF_Results_Path if it exists (ensure it comes before file paths)
            for special_col in special_cols:
                if special_col in df.columns and special_col not in all_cols:
                    all_cols.append(special_col)
            
            # Add file path columns at the end
            all_cols.extend(file_path_cols)
            
            # Rename plan_number column
            df = df.rename(columns={f'{entry_type.lower()}_number': 'plan_number'})
            
            # Fill in missing columns with None
            for col in all_cols:
                if col not in df.columns:
                    df[col] = None
            
            # Make sure full_path column is preserved and included
            if 'full_path' in df.columns and 'full_path' not in all_cols:
                all_cols.append('full_path')
            
            # Return DataFrame with specified column order
            cols_to_return = [col for col in all_cols if col in df.columns]
            return df[cols_to_return]
        
        return df

    @log_call
    def _get_prj_entries(self, entry_type):
        """
        Extract entries of a specific type from the HEC-RAS project file.
        """
        entries = []
        pattern = re.compile(rf"{entry_type} File=(\w+)")

        try:
            with open(self.prj_file, 'r') as file:
                for line in file:
                    match = pattern.match(line.strip())
                    if match:
                        file_name = match.group(1)
                        full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
                        entry = self._create_entry(entry_type, file_name, full_path)
                        entries.append(entry)
        
            return self._format_dataframe(pd.DataFrame(entries), entry_type)
        
        except Exception as e:
            logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
            raise

    def _create_entry(self, entry_type, file_name, full_path):
        """Helper method to create entry dictionary."""
        entry_number = file_name[1:]
        entry = {
            f'{entry_type.lower()}_number': entry_number,
            'full_path': full_path,
            'unsteady_number': None,
            'geometry_number': None
        }
        
        if entry_type == 'Unsteady':
            entry['unsteady_number'] = entry_number
            entry.update(self._parse_unsteady_file(Path(full_path)))
        elif entry_type == 'Plan':
            self._update_plan_entry(entry, entry_number, full_path)
        
        return entry

    def _update_plan_entry(self, entry, entry_number, full_path):
        """Helper method to update plan entry with additional information."""
        plan_info = self._parse_plan_file(Path(full_path))
        if plan_info:
            # Handle Flow File
            flow_file = plan_info.get('Flow File')
            if flow_file:
                if flow_file.startswith('u'):
                    entry.update({'unsteady_number': flow_file[1:], 'Flow File': flow_file[1:]})
                else:
                    entry['Flow File'] = flow_file[1:] if flow_file.startswith('f') else None
            
            # Handle Geom File
            geom_file = plan_info.get('Geom File')
            if geom_file and geom_file.startswith('g'):
                entry.update({'geometry_number': geom_file[1:], 'Geom File': geom_file[1:]})
            
            # Add remaining plan info
            entry.update({k: v for k, v in plan_info.items() if k not in ['Flow File', 'Geom File']})
            
            # Add HDF results path
            hdf_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
            entry['HDF_Results_Path'] = str(hdf_path) if hdf_path.exists() else None


# Create a global instance named 'ras'
# Defining the global instance allows the init_ras_project function to initialize the project.
# This only happens on the library initialization, not when the user calls init_ras_project.
ras = RasPrj()

# END OF CLASS DEFINITION


# START OF FUNCTION DEFINITIONS

@log_call
def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
    """
    Initialize a RAS project for use with the ras-commander library.

    This is the primary function for setting up a HEC-RAS project. It:
    1. Finds the project file (.prj) in the specified folder
    2. Identifies the appropriate HEC-RAS executable
    3. Loads project data (plans, geometries, flows)
    4. Creates dataframes containing project components

    Args:
        ras_project_folder (str or Path): The path to the RAS project folder.
        ras_version (str, optional): The version of RAS to use (e.g., "6.6") OR
                                     a full path to the Ras.exe file (e.g., "D:/Programs/HEC/HEC-RAS/6.6/Ras.exe").
                                     If None, will attempt to detect from plan files.
        ras_object (RasPrj, optional): If None, updates the global 'ras' object.
                                       If a RasPrj instance, updates that instance.
                                       If any other value, creates and returns a new RasPrj instance.

    Returns:
        RasPrj: An initialized RasPrj instance.
        
    Raises:
        FileNotFoundError: If the specified project folder doesn't exist.
        ValueError: If no HEC-RAS project file is found in the folder.
        
    Example:
        >>> # Initialize using the global 'ras' object (most common)
        >>> init_ras_project("/path/to/project", "6.6")
        >>> print(f"Initialized project: {ras.project_name}")
        >>>
        >>> # Create a new RasPrj instance
        >>> my_project = init_ras_project("/path/to/project", "6.6", "new")
        >>> print(f"Created project instance: {my_project.project_name}")
    """
    # Convert to absolute path immediately to ensure consistent path handling
    project_folder = Path(ras_project_folder).resolve()
    if not project_folder.exists():
        logger.error(f"The specified RAS project folder does not exist: {project_folder}")
        raise FileNotFoundError(f"The specified RAS project folder does not exist: {project_folder}. Please check the path and try again.")

    # Determine which RasPrj instance to use
    if ras_object is None:
        # Use the global 'ras' object
        logger.debug("Initializing global 'ras' object via init_ras_project function.")
        ras_object = ras
    elif not isinstance(ras_object, RasPrj):
        # Create a new RasPrj instance
        logger.debug("Creating a new RasPrj instance.")
        ras_object = RasPrj()
    
    ras_exe_path = None
    
    # Use version specified by user if provided
    if ras_version is not None:
        ras_exe_path = get_ras_exe(ras_version)
        if ras_exe_path == "Ras.exe" and ras_version != "Ras.exe":
            logger.warning(f"HEC-RAS Version {ras_version} was not found. Running HEC-RAS will fail.")
    else:
        # No version specified, try to detect from plan files
        detected_version = None
        logger.info("No HEC-RAS Version Specified.Attempting to detect HEC-RAS version from plan files.")
        
        # Look for .pXX files in project folder
        logger.info(f"Searching for plan files in {project_folder}")
        # Search for any file with .p01 through .p99 extension, regardless of base name
        plan_files = list(project_folder.glob("*.p[0-9][0-9]"))
        
        if not plan_files:
            logger.info(f"No plan files found in {project_folder}")
        
        for plan_file in plan_files:
            logger.info(f"Found plan file: {plan_file.name}")
            content, encoding = read_file_with_fallback_encoding(plan_file)
            
            if not content:
                logger.info(f"Could not read content from {plan_file.name}")
                continue
                
            logger.info(f"Successfully read plan file with {encoding} encoding")
            
            # Look for Program Version in plan file
            for line in content.splitlines():
                if line.startswith("Program Version="):
                    version = line.split("=")[1].strip()
                    logger.info(f"Found Program Version={version} in {plan_file.name}")
                    
                    # Replace 00 in version string if present
                    if "00" in version:
                        version = version.replace("00", "0")
                    
                    # Try to get RAS executable for this version
                    test_exe_path = get_ras_exe(version)
                    logger.info(f"Checking RAS executable path: {test_exe_path}")
                    
                    if test_exe_path != "Ras.exe":
                        detected_version = version
                        ras_exe_path = test_exe_path
                        logger.debug(f"Found valid HEC-RAS version {version} in plan file {plan_file.name}")
                        break
                    else:
                        logger.info(f"Version {version} not found in default installation path")
            
            if detected_version:
                break
        
        if not detected_version:
            logger.error("No valid HEC-RAS version found in any plan files.")
            ras_exe_path = "Ras.exe"
            logger.warning("No valid HEC-RAS version was detected. Running HEC-RAS will fail.")
    
    # Initialize or re-initialize with the determined executable path
    ras_object.initialize(project_folder, ras_exe_path)
    
    # Always update the global ras object as well
    if ras_object is not ras:
        ras.initialize(project_folder, ras_exe_path)
        logger.debug("Global 'ras' object also updated to match the new project.")
    
    logger.debug(f"Project initialized. Project folder: {ras_object.project_folder}")
    logger.debug(f"Using HEC-RAS executable: {ras_exe_path}")
    return ras_object

@log_call
def get_ras_exe(ras_version=None):
    """
    Determine the HEC-RAS executable path based on the input.
    
    This function attempts to find the HEC-RAS executable in the following order:
    1. If ras_version is a valid file path to an .exe file, use that path directly
       (useful for non-standard installations or non-C: drive installations)
    2. If ras_version is a known version number, use default installation path (on C: drive)
    3. If global 'ras' object has ras_exe_path, use that
    4. As a fallback, return "Ras.exe" but log an error
    
    Args:
        ras_version (str, optional): Either a version number (e.g., "6.6") or 
                                     a full path to the HEC-RAS executable 
                                     (e.g., "D:/Programs/HEC/HEC-RAS/6.6/Ras.exe").
    
    Returns:
        str: The full path to the HEC-RAS executable or "Ras.exe" if not found.
    
    Note:
        - HEC-RAS version numbers include: "6.6", "6.5", "6.4.1", "6.3", etc.
        - The default installation path follows: C:/Program Files (x86)/HEC/HEC-RAS/{version}/Ras.exe
        - For non-standard installations, provide the full path to Ras.exe
        - Returns "Ras.exe" if no valid path is found, with error logged
        - Allows the library to function even without HEC-RAS installed
    """
    if ras_version is None:
        if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
            logger.debug(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
            return ras.ras_exe_path
        else:
            default_path = "Ras.exe"
            logger.debug(f"No HEC-RAS version specified and global 'ras' object not initialized or missing ras_exe_path.")
            logger.warning(f"HEC-RAS is not installed or version not specified. Running HEC-RAS will fail unless a valid installed version is specified.")
            return default_path
    
    ras_version_numbers = [
        "6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
        "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
        "4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
    ]
    
    # Check if input is a direct path to an executable
    hecras_path = Path(ras_version)
    if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
        logger.debug(f"HEC-RAS executable found at specified path: {hecras_path}")
        return str(hecras_path)
    
    # Check known version numbers
    if str(ras_version) in ras_version_numbers:
        default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
        if default_path.is_file():
            logger.debug(f"HEC-RAS executable found at default path: {default_path}")
            return str(default_path)
        else:
            error_msg = f"HEC-RAS Version {ras_version} is not found at expected path. Running HEC-RAS will fail unless a valid installed version is specified."
            logger.error(error_msg)
            return "Ras.exe"
    
    # Try to handle other version formats (e.g., just the number without dots)
    try:
        # First check if it's a direct version number
        version_str = str(ras_version)
        
        # Check for paths like "C:/Path/To/Ras.exe"
        if os.path.sep in version_str and version_str.lower().endswith('.exe'):
            exe_path = Path(version_str)
            if exe_path.is_file():
                logger.debug(f"HEC-RAS executable found at specified path: {exe_path}")
                return str(exe_path)
        
        # Try to find a matching version from our list
        for known_version in ras_version_numbers:
            if version_str in known_version or known_version.replace('.', '') == version_str:
                default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{known_version}/Ras.exe")
                if default_path.is_file():
                    logger.debug(f"HEC-RAS executable found at default path: {default_path}")
                    return str(default_path)
        
        # Check if it's a newer version
        if '.' in version_str:
            major_version = int(version_str.split('.')[0])
            if major_version >= 6:
                default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
                if default_path.is_file():
                    logger.debug(f"HEC-RAS executable found at path for newer version: {default_path}")
                    return str(default_path)
    except Exception as e:
        logger.error(f"Error parsing version or finding path: {e}")
    
    error_msg = f"HEC-RAS Version {ras_version} is not recognized or installed. Running HEC-RAS will fail unless a valid installed version is specified."
    logger.error(error_msg)
    return "Ras.exe"

==================================================

