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¶
- Unit tests - Test individual methods
- Integration tests - Test workflows
- Example projects - Real HMS models for testing
- HMS version matrix - Test against 3.x and 4.x
Related Topics¶
- Style Guide - Coding standards
- Contributing - Development workflow
- LLM Forward Overview - Design philosophy
Architecture prioritizes simplicity, maintainability, and LLM Forward principles.