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