Module Integration
This guide explains how to add new solver modules to pyhnhtools following the established architecture pattern.
Architecture Pattern
All modules follow a three-layer integration pattern:
qt_gui.py (Generic GUI)
↓
{module}_gui.py (Integration Layer)
↓
{module}_adapter.py (Data Adapter)
↓
{module}/ (Core Solver)
Layer Responsibilities
Layer 1: qt_gui.py (Generic GUI Framework)
Window layout and widget management
Tab organization
User interaction routing
Common visualization patterns
Does NOT contain: Solver-specific logic
Layer 2: {module}_gui.py (Integration Interface)
Module workflow orchestration
Converting GUI inputs to solver inputs
Converting solver outputs to GUI displays
Module-specific plotting and validation
Example: std_step_1d_gui.py contains StandardStep1DInterface class
Layer 3: {module}_adapter.py (Data Adapter)
Model serialization (JSON, CSV)
Data format transformation
File I/O operations
Does NOT contain: GUI knowledge or solver details
Layer 4: {module}/ (Core Solver)
Pure computation
Algorithm implementation
Physics/mathematics
Does NOT contain: GUI or I/O logic
Step-by-Step Implementation
Step 1: Create the Integration Interface
Copy the template:
cp src/pyhnhtools/gui/std_step_1d_gui.py src/pyhnhtools/gui/culverts_gui.py
Rename the class:
# Change from:
class StandardStep1DInterface:
# To:
class CulvertsInterface:
Adapt the methods to your workflow:
class CulvertsInterface:
def __init__(self, gui_widget):
self.gui = gui_widget
def setup(self):
"""Initialize the culverts interface."""
try:
from pyhnhtools.culverts import analyze_inlet
self.analyze_inlet_func = analyze_inlet
return True
except ImportError:
return False
def create_config(self, diameter, length, manning_n):
"""Create configuration from parameters."""
config = CulvertConfig(diameter, length, manning_n)
self.config = config
return config
def run_analysis(self):
"""Execute culvert analysis."""
results = self.analyze_inlet_func(self.config)
self._display_results(results)
return True
Step 2: Create or Adapt the Data Adapter
If your module doesn’t have an adapter, create one:
# File: src/pyhnhtools/culverts_adapter.py
import json
from pyhnhtools.culverts import CulvertConfig
def create_config(params):
"""Create configuration from dict."""
return CulvertConfig(**params)
def run_analysis(config):
"""Run analysis and return results."""
# Call your core solver
results = config.analyze()
return results
def save_results(results, filepath):
"""Save results to JSON."""
with open(filepath, 'w') as f:
json.dump(results.to_dict(), f)
Step 3: Register in qt_gui.py
Add import:
from pyhnhtools.gui.culverts_gui import CulvertsInterface
Instantiate in BackwaterWidget:
class BackwaterWidget(QMainWindow):
def __init__(self):
# ... existing code ...
# Initialize culverts interface
self.culverts_interface = CulvertsInterface(self)
self.culverts_interface.setup()
Create tab:
def create_culverts_tab(self):
tab = QWidget()
layout = QVBoxLayout()
# Add culvert-specific controls
# (diameter spinbox, length input, Manning's n, etc.)
self.tabs.addTab(tab, "Culvert Analysis")
Wire button clicks:
def on_run_clicked(self):
current_tab = self.tabs.currentIndex()
if current_tab == 1: # Culverts tab
self.culverts_interface.run_analysis()
Template Interface Class
Use this as a starting point:
from typing import Optional, Dict, List
import logging
logger = logging.getLogger(__name__)
class {Module}Interface:
"""Integration between GUI and {Module} solver."""
def __init__(self, gui_widget):
self.gui = gui_widget
self.model = None
self.results = None
def setup(self) -> bool:
"""Initialize interface. Call once at startup."""
try:
# Import your module
from pyhnhtools.{module} import YourClass
self.YourClass = YourClass
logger.info(f"{module} interface initialized")
return True
except ImportError as e:
logger.error(f"Failed to initialize: {e}")
self.gui.text_out.setText(f"Error: {e}")
return False
def load_model(self, filepath: str) -> bool:
"""Load model from file."""
try:
# Load and parse
self.model = load_from_file(filepath)
self._update_display()
return True
except Exception as e:
logger.error(f"Load failed: {e}")
self.gui.text_out.setText(f"Error: {e}")
return False
def new_model(self) -> bool:
"""Create new empty model."""
try:
self.model = self.YourClass()
self.gui.text_out.setText("New model created")
return True
except Exception as e:
logger.error(f"Create failed: {e}")
return False
def run_solver(self) -> bool:
"""Execute solver."""
if not self.validate_model():
return False
try:
self.results = self.model.solve()
self._display_results()
return True
except Exception as e:
logger.error(f"Solver failed: {e}")
return False
def validate_model(self) -> bool:
"""Check model validity."""
if not self.model:
self.gui.text_out.setText("No model")
return False
# Add validation logic
return True
def save_model(self, filepath: str) -> bool:
"""Save model to file."""
try:
save_to_file(self.model, filepath)
return True
except Exception as e:
logger.error(f"Save failed: {e}")
return False
def _display_results(self):
"""Update GUI with results."""
if not self.results:
return
# Update GUI elements
self.gui.text_out.setText("Results ready")
Design Patterns
Pattern 1: Model Loading
def load_model(self, filepath):
with open(filepath) as f:
data = json.load(f)
self.model = Model(**data)
self._update_display()
Pattern 2: Error Handling
try:
result = self.solver.compute()
except ValueError as e:
logger.error(f"Computation error: {e}")
self.gui.text_out.setText(f"Error: {e}")
return False
Pattern 3: Workflow State
self.model = None # Not loaded yet
self.results = None # Not computed yet
self.interface = None # Not initialized
Pattern 4: GUI Updates
# Access GUI through reference
self.gui.table_results.setRowCount(10)
self.gui.text_out.setText("Analysis complete")
self.gui.plot_widget.update()
Testing Your Module
Unit Tests
# tests/test_culverts_gui.py
def test_interface_setup():
widget = MockWidget()
interface = CulvertsInterface(widget)
assert interface.setup() == True
def test_create_config():
widget = MockWidget()
interface = CulvertsInterface(widget)
config = interface.create_config(diameter=24, length=100)
assert config.diameter == 24
Integration Tests
# Test loading GUI with new module
python -m pyhnhtools.gui.app
# Check that new tab appears
# Check that buttons work
# Check that solver runs
Common Issues
ImportError: No module named {module}
Ensure module is installed:
pip install -e .
GUI doesn’t show new tab
Check:
Tab creation called in BackwaterWidget.__init__()
Tab is added to self.tabs
Index correct in on_run_clicked()
Solver doesn’t run
Check:
interface.setup() returned True
validate_model() passes
All required data set
Results don’t display
Check:
_display_results() called after solver
GUI element references correct
Results object has expected fields
File Checklist
When adding a new module:
[ ] {module}_gui.py created
[ ] {module}_adapter.py created (if needed)
[ ] qt_gui.py updated with imports
[ ] qt_gui.py tab creation added
[ ] Button click routing updated
[ ] Tests added
[ ] Documentation updated
[ ] CHANGELOG.md updated
[ ] Git commit created
Documentation Updates
Add documentation for your module:
# docs/user_guide/{module}.rst
{Module} Analysis
=================
.. automodule:: pyhnhtools.gui.{module}_gui
:members:
Usage
-----
.. code-block:: python
# Your usage example
Update index.rst:
.. toctree::
:maxdepth: 2
user_guide/{module}
See Also
Templates & Patterns - Template reference files
Contributing - Contributing guidelines
Core API - API reference