Skip to content

Architecture

Technical architecture and design decisions for HMS Commander.

Overview

HMS Commander is designed around three core principles: 1. Static class architecture - No instantiation required for most operations 2. DataFrame-based interfaces - Pandas DataFrames for data manipulation 3. File-based operations - Direct manipulation of HMS text files

System Architecture

┌─────────────────────────────────────────────────────────────┐
│                     HMS Commander                            │
├─────────────────────────────────────────────────────────────┤
│  Project Layer                                              │
│  ┌────────────┐        ┌──────────────────────────┐        │
│  │  HmsPrj    │──────→ │ Project DataFrames       │        │
│  │  (global)  │        │ - basin_df, met_df, etc. │        │
│  └────────────┘        └──────────────────────────┘        │
├─────────────────────────────────────────────────────────────┤
│  File Operations Layer (Static Classes)                     │
│  ┌───────────┬───────────┬────────────┬──────────┐         │
│  │ HmsBasin  │ HmsMet    │ HmsControl │ HmsGage  │         │
│  └───────────┴───────────┴────────────┴──────────┘         │
│  ┌───────────┬───────────┬────────────┐                    │
│  │ HmsGeo    │ HmsRun    │ HmsUtils   │                    │
│  └───────────┴───────────┴────────────┘                    │
├─────────────────────────────────────────────────────────────┤
│  Execution Layer                                            │
│  ┌───────────┬───────────────────────────────────┐         │
│  │ HmsCmdr   │ HmsJython                         │         │
│  │ (compute) │ (script generation)               │         │
│  └───────────┴───────────────────────────────────┘         │
├─────────────────────────────────────────────────────────────┤
│  Data Layer                                                 │
│  ┌───────────┬──────────────────────────────────┐          │
│  │ HmsDss    │ HmsResults                       │          │
│  │ (RasDss)  │ (analysis)                       │          │
│  └───────────┴──────────────────────────────────┘          │
├─────────────────────────────────────────────────────────────┤
│  Parsing Layer                                              │
│  ┌──────────────┬─────────────┐                            │
│  │ HmsFileParser│ _constants  │                            │
│  └──────────────┴─────────────┘                            │
└─────────────────────────────────────────────────────────────┘
         │                        │
         ▼                        ▼
┌─────────────────┐    ┌──────────────────────┐
│  HMS Text Files │    │  HEC-HMS Executable  │
│  (.basin, .met) │    │  (Jython interface)  │
└─────────────────┘    └──────────────────────┘

Core Design Patterns

1. Static Class Pattern

Why: HMS operations are stateless - they work directly with files, no object state needed.

# Static methods - no instantiation
HmsBasin.get_subbasins("model.basin")  # ✅ Correct

# NOT this:
basin = HmsBasin()  # ❌ Wrong - raises error
basin.get_subbasins("model.basin")

Benefits: - Simple, intuitive API - No state management - Easy to test - Clear functional boundaries

2. Global Project Object Pattern

Why: Most users work with one project at a time.

from hms_commander import init_hms_project, hms

# Initialize global project
init_hms_project(r"C:\path\to\project")

# Use global object
print(hms.basin_df)
print(hms.met_df)

# Multi-project support still available
project1 = HmsPrj()
init_hms_project(r"C:\project1", hms_object=project1)

Benefits: - Simple for common use case - Optional multi-project support - Backward compatible

3. DataFrame Interface Pattern

Why: Engineers are familiar with spreadsheet-like data.

# All project data in DataFrames
basins_df = hms.basin_df
mets_df = hms.met_df

# Standard pandas operations
basins_df[basins_df['name'].str.contains('Sub')]
mets_df.sort_values('name')

Benefits: - Familiar interface - Powerful filtering/sorting - Easy export to Excel/CSV - Integration with data science tools

4. File Parser Pattern

Why: HMS files are ASCII text with consistent structure.

# Shared parsing utilities
from hms_commander._parsing import HmsFileParser

# Read with encoding fallback
content = HmsFileParser.read_file("model.basin")

# Parse named blocks
blocks = HmsFileParser.parse_blocks(content, "Subbasin")

# Update parameters
updated = HmsFileParser.update_parameter(content, "Area", "100.0")

Benefits: - DRY principle - Consistent error handling - Centralized encoding management - Reusable across all file types

5. Clone Workflow Pattern

Why: Non-destructive operations critical for QAQC.

# Template → Clone → Modify
HmsBasin.clone_basin("Original", "Modified")
HmsBasin.set_loss_parameters("Modified.basin", "Sub1", curve_number=85)

# Original untouched, modification in clone

Implementation:

@staticmethod
def clone_basin(template, new_name, description=None, hms_object=None):
    # 1. Use HmsUtils.clone_file() for file copy
    # 2. Update internal references
    # 3. Add metadata (description)
    # 4. Register in project file
    # 5. Update hms_object DataFrames

Benefits: - Preserves originals - Side-by-side comparison - Rollback capability - Audit trail

Module Organization

hms_commander/
├── __init__.py           # Package exports
├── HmsPrj.py            # Project management
├── HmsBasin.py          # Basin operations
├── HmsMet.py            # Met operations
├── HmsControl.py        # Control operations
├── HmsGage.py           # Gage operations
├── HmsRun.py            # Run configuration
├── HmsGeo.py            # Geospatial operations
├── HmsCmdr.py           # Execution engine
├── HmsJython.py         # Jython scripting
├── HmsDss.py            # DSS operations
├── HmsResults.py        # Results analysis
├── HmsUtils.py          # Utilities
├── HmsExamples.py       # Example management
├── _parsing.py          # Internal parsing
├── _constants.py        # Constants
├── Decorators.py        # @log_call etc.
└── LoggingConfig.py     # Logging setup

Why No Submodules?

From STYLE_GUIDE.md:

Use submodules when: - 5+ related classes - Optional dependencies - Experimental features - Large utilities (500+ lines)

Don't use submodules when: - Single purpose classes ✅ (HMS Commander case) - Few functions - Premature organization

Decision: HMS Commander has single-purpose classes that don't meet the threshold for submodule complexity.

Execution Model

Jython Script Generation

HMS Commander doesn't execute HEC-HMS directly. Instead:

# 1. Generate Jython script
script = HmsJython.generate_compute_script("project", "Run 1")

# 2. Execute via HEC-HMS
HmsJython.execute_script(script, hms_exe_path)

# 3. HEC-HMS runs Jython in embedded interpreter

Why Jython? - Official HEC-HMS automation interface - Supports HMS 3.x and 4.x - Version detection automatic

Version Detection

# Auto-detects HMS version from path
def execute_script(script, hms_exe_path, max_memory=None):
    if "3." in str(hms_exe_path):
        # HMS 3.x: hec-hms.cmd in root
        java_dir = "java/bin"
    else:
        # HMS 4.x: hec-hms.cmd in bin/
        java_dir = "../jre/bin"

Data Flow

1. Project Initialization

User calls init_hms_project()
Scan project folder for .hms file
Parse .hms to find all component files
Create DataFrames for each component type
Store in HmsPrj object (global hms)

2. File Operations

User calls HmsBasin.get_subbasins(path)
HmsFileParser.read_file() with encoding fallback
HmsFileParser.parse_blocks("Subbasin")
Extract parameters to dictionary
Convert to pandas DataFrame
Return to user

3. Execution

User calls HmsCmdr.compute_run("Run 1")
HmsJython.generate_compute_script()
Determine HMS version from hms.exe_path
Generate Python 2 or 3 compatible script
Write temporary script file
Execute: hec-hms.cmd -script temp.py
Monitor stdout/stderr
Clean up temporary file
Return success/failure status

Error Handling Strategy

Encoding Fallback

# Primary: UTF-8
# Fallback: Latin-1 (CP1252)
try:
    with open(file_path, 'r', encoding='utf-8') as f:
        return f.read()
except UnicodeDecodeError:
    with open(file_path, 'r', encoding='latin-1') as f:
        return f.read()

Logging Decorator

@log_call
def some_function(param1, param2):
    # Automatically logs:
    # - Function name
    # - Parameters
    # - Return value
    # - Execution time
    # - Any exceptions
    pass

Path Validation

# Always use pathlib.Path
basin_path = Path(basin_path).resolve()

# Validate existence
if not basin_path.exists():
    raise FileNotFoundError(f"Basin file not found: {basin_path}")

Testing Strategy

  1. Unit tests - Test individual methods
  2. Integration tests - Test workflows
  3. Example projects - Real HMS models for testing
  4. HMS version matrix - Test against 3.x and 4.x

Architecture prioritizes simplicity, maintainability, and LLM Forward principles.

CLB Engineering Corporation  ·  LLM Forward Engineering
HMS Commander is a free and open-source project maintained by CLB Engineering Corporation. For agencies and firms seeking to modernize H&H workflows with LLM Forward approaches, contact CLB to partner with the engineers who wrote the automation.