Skip to content

4. MVP SimPy Integration

Overview

Location: popupsim/backend/src/contexts/shared/infrastructure/simpy_adapter.py

SimPy provides discrete event simulation for the Retrofit Workflow Context. Integration follows a thin adapter pattern to isolate SimPy dependencies.

Architecture

┌─────────────────────────────────────────┐
│   Retrofit Workflow Context            │
│   - Coordinators (Application)         │
│   - Domain Services (No SimPy)         │
└─────────────────┬───────────────────────┘
                  │
                  │ Uses
                  │
┌─────────────────▼───────────────────────┐
│   SimPy Adapter (Shared/Infrastructure) │
│   - Environment wrapper                 │
│   - Process management                  │
└─────────────────┬───────────────────────┘
                  │
                  │
┌─────────────────▼───────────────────────┐
│   SimPy Framework                       │
│   - Environment, Resource, Process      │
└─────────────────────────────────────────┘

SimPy Adapter

File: contexts/shared/infrastructure/simpy_adapter.py

import simpy
from typing import Any, Callable, Generator

class SimPyAdapter:
    """Thin adapter for SimPy environment."""

    def __init__(self, env: simpy.Environment):
        self.env = env

    @property
    def now(self) -> float:
        """Current simulation time."""
        return self.env.now

    def timeout(self, delay: float) -> Any:
        """Wait for delay time units."""
        return self.env.timeout(delay)

    def process(self, generator: Generator) -> Any:
        """Register a process."""
        return self.env.process(generator)

    def run(self, until: float | None = None) -> None:
        """Run simulation."""
        self.env.run(until=until)

    @staticmethod
    def create_environment() -> simpy.Environment:
        """Create SimPy environment."""
        return simpy.Environment()

Coordinator Pattern

Coordinators use SimPy generators for discrete event simulation:

File: contexts/retrofit_workflow/application/coordinators/collection_coordinator.py

from typing import Generator, Any

class CollectionCoordinator:
    """Coordinates wagon collection."""

    def start(self) -> None:
        """Start coordinator process."""
        self.config.env.process(self._collection_process())

    def _collection_process(self) -> Generator[Any, Any, None]:
        """Main collection loop."""
        while True:
            # Wait for wagon
            wagon = yield self.config.collection_queue.get()

            # Collect batch
            wagons = yield from self._collect_batch(wagon)

            # Transport
            yield from self._transport_batch(wagons)

Resource Management

SimPy Resources manage limited capacity:

File: contexts/retrofit_workflow/infrastructure/resource_managers/locomotive_resource_manager.py

import simpy
from typing import Any

class LocomotiveResourceManager:
    """Manages locomotive resources."""

    def __init__(self, env: simpy.Environment, locomotives: list[Locomotive]):
        self.env = env
        self.resource = simpy.Resource(env, capacity=len(locomotives))
        self.locomotives = {loco.locomotive_id: loco for loco in locomotives}

    def allocate(self) -> Any:
        """Allocate locomotive (blocks until available)."""
        return self.resource.request()

    def release(self, request: Any) -> None:
        """Release locomotive."""
        self.resource.release(request)

Event Bus Integration

External Trains Context publishes events via SimPy:

File: contexts/external_trains/application/external_trains_context.py

def _arrival_process(self, train: Train, arrival_time: float) -> Generator[Any, Any, None]:
    """Process single train arrival."""
    yield self.env.timeout(arrival_time)

    # Publish event
    event = TrainArrivedEvent(
        train_id=train.id,
        wagons=train.wagons,
        arrival_time=self.env.now
    )
    self.event_bus.publish(event)

Testing

Unit Tests (No SimPy)

Domain services don't depend on SimPy:

def test_batch_formation() -> None:
    """Test without SimPy."""
    service = BatchFormationService()
    wagons = [Wagon(...) for _ in range(5)]

    assert service.can_form_batch(wagons, min_size=1, max_size=10)
    batch = service.form_batch(wagons, batch_size=5)
    assert len(batch) == 5

Integration Tests (With SimPy)

def test_collection_coordinator() -> None:
    """Test with SimPy."""
    env = simpy.Environment()
    queue = simpy.Store(env)

    coordinator = CollectionCoordinator(...)
    coordinator.start()

    # Add wagon
    queue.put(Wagon(...))

    # Run simulation
    env.run(until=100)

    # Verify wagon processed
    assert len(coordinator.processed_wagons) == 1

Best Practices

✅ Do's

  • Keep domain logic SimPy-free
  • Use generators for coordinators
  • Isolate SimPy in infrastructure layer
  • Test domain logic without SimPy

❌ Don'ts

  • Don't import SimPy in domain services
  • Don't put business logic in generators
  • Don't use global SimPy resources