Skip to content

5. Building Block View

5.1 Level 1: System Context

PopUpSim consists of 4 bounded contexts following Domain-Driven Design principles.

graph TB
    subgraph "PopUpSim System"
        CFG["Configuration Context<br/>File loading & validation"]
        RWF["Retrofit Workflow Context<br/>Core simulation logic"]
        RLY["Railway Infrastructure Context<br/>Track management"]
        EXT["External Trains Context<br/>Train arrivals"]
    end

    subgraph "External"
        Files["Configuration Files<br/>JSON/CSV"]
        Output["Results<br/>CSV/JSON"]
    end

    Files -->|Load| CFG
    CFG -->|Scenario| RWF
    CFG -->|Scenario| RLY
    CFG -->|Scenario| EXT
    RLY <-->|Track state| RWF
    EXT -->|Train arrivals| RWF
    RWF -->|Export| Output

    classDef context fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef external fill:#f3e5f5,stroke:#4a148c,stroke-width:2px

    class CFG,RWF,RLY,EXT context
    class Files,Output external

Contained Building Blocks

Context Responsibility Technology
Configuration Load and validate scenario files Pydantic, Pandas
Retrofit Workflow Execute simulation, coordinate wagon flow SimPy, domain services
Railway Infrastructure Manage track capacity and occupancy Domain aggregates
External Trains Handle train arrivals and wagon creation Event publishing

Key Interfaces

Interface Source Target Description
Scenario Configuration All contexts Validated configuration data
Track Operations Railway Infrastructure Retrofit Workflow Track capacity queries and wagon placement
Train Arrivals External Trains Retrofit Workflow TrainArrivedEvent with wagons
Event Bus All contexts All contexts Domain event communication

5.2 Level 2: Configuration Context

Responsibility

Load scenario configuration from files and validate using Pydantic models.

graph TB
    subgraph "Configuration Context"
        Builder["ConfigurationBuilder<br/>Entry point"]
        Loader["FileLoader<br/>File parsing"]
        Models["Domain Models<br/>Pydantic validation"]
    end

    Files["JSON/CSV Files"] --> Builder
    Builder --> Loader
    Loader --> Models
    Models --> Scenario["Validated Scenario"]

    classDef component fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef external fill:#f3e5f5,stroke:#4a148c,stroke-width:2px

    class Builder,Loader,Models component
    class Files,Scenario external

Components

Component Responsibility Implementation
ConfigurationBuilder Load scenario from file path configuration_builder.py
FileLoader Parse JSON/CSV files, handle references file_loader.py
Scenario Root configuration model with validation scenario.py
ProcessTimes Timing configuration for operations process_times.py
DTOs Input data transfer objects dtos/

Code Example

from pathlib import Path
from contexts.configuration.domain.configuration_builder import ConfigurationBuilder

# Load scenario
scenario = ConfigurationBuilder(Path("scenario_dir")).build()

# Access configuration
print(f"Scenario: {scenario.id}")
print(f"Workshops: {len(scenario.workshops)}")
print(f"Trains: {len(scenario.trains)}")

5.3 Level 2: Retrofit Workflow Context

Responsibility

Execute discrete event simulation coordinating wagon flow through the retrofit process.

graph TB
    subgraph "Retrofit Workflow Context"
        Context["RetrofitWorkflowContext<br/>Initialization & orchestration"]
        Arrival["ArrivalCoordinator<br/>Process train arrivals"]
        Collection["CollectionCoordinator<br/>Move to retrofit track"]
        Workshop["WorkshopCoordinator<br/>Retrofit operations"]
        Parking["ParkingCoordinator<br/>Move to parking"]
        Services["Domain Services<br/>Business logic"]
        Resources["Resource Managers<br/>Locomotives, tracks, workshops"]
    end

    Context --> Arrival
    Context --> Collection
    Context --> Workshop
    Context --> Parking
    Arrival --> Services
    Collection --> Services
    Workshop --> Services
    Parking --> Services
    Services --> Resources

    classDef component fill:#e1f5fe,stroke:#01579b,stroke-width:2px

    class Context,Arrival,Collection,Workshop,Parking,Services,Resources component

Components

Coordinators (Application Layer)

Coordinator Responsibility SimPy Process
ArrivalCoordinator Receive trains, classify wagons, distribute to collection tracks Yes
CollectionCoordinator Form batches, allocate locomotive, transport to retrofit track Yes
WorkshopCoordinator Assign wagons to workshops, execute retrofit, return to retrofitted track Yes
ParkingCoordinator Transport completed wagons to parking tracks Yes

Domain Services (No SimPy Dependencies)

Core Formation Services: | Service | Responsibility | |---------|----------------| | BatchFormationService | Create wagon batches based on capacity constraints | | RakeFormationService | Form and dissolve wagon rakes with coupling logic | | TrainFormationService | Assemble trains (locomotive + rake) with preparation times |

Scheduling & Assignment: | Service | Responsibility | |---------|----------------| | WorkshopSchedulingService | Schedule wagon batches to available workshops | | WorkshopAssignmentService | Assign wagons to workshop bays | | WorkshopAssignmentStrategies | Workshop assignment strategy implementations |

Transport & Routing: | Service | Responsibility | |---------|----------------| | TransportPlanningService | Plan transport operations between tracks | | RouteService | Provide route durations between tracks |

Selection Services: | Service | Responsibility | |---------|----------------| | ResourceSelectionService | Select resources using strategies (first_available, least_occupied, round_robin) | | TrackSelectionService | Select tracks with capacity constraints | | ParkingTrackSelectionService | Select parking tracks for completed wagons |

Coupling & Assembly: | Service | Responsibility | |---------|----------------| | CouplingService | Calculate coupling/decoupling times based on coupler types | | CouplingValidationService | Validate coupling compatibility between wagons | | TrainAssemblyService | Assemble locomotives to wagon rakes |

Lifecycle Management: | Service | Responsibility | |---------|----------------| | RakeLifecycleManager | Manage complete rake lifecycle (form, transport, dissolve) | | WagonFactoryService | Create wagon entities from configuration | | WagonEligibilityService | Check wagon eligibility for operations | | WagonAccumulationService | Accumulate wagons for batch formation |

Event Generation: | Service | Responsibility | |---------|----------------| | RejectionEventFactory | Create rejection events for wagons that cannot be processed |

Key Principle: All domain services are pure business logic with no SimPy dependencies.

Application Services (Orchestration Layer)

Operation Services: | Service | Responsibility | |---------|----------------| | RakeOperationsService | Orchestrate rake formation, transport, and dissolution operations | | WorkshopOperationsService | Orchestrate workshop assignment and processing operations | | ParkingTransportService | Orchestrate transport to parking tracks | | RailwayOperationsService | Orchestrate railway infrastructure operations |

Coordination Services: | Service | Responsibility | |---------|----------------| | CoordinationService | Coordinate between multiple coordinators | | LocomotiveCoordinationService | Coordinate locomotive allocation across operations |

Metrics & Collection: | Service | Responsibility | |---------|----------------| | MetricsAggregator | Aggregate simulation metrics from all coordinators | | EventCollectionService | Collect and export simulation events | | DualStreamCollector | Collect dual-stream events (state + location) | | DualStreamAdapter | Adapt events to dual-stream format |

Resource Managers (Infrastructure Layer)

Manager Responsibility
LocomotiveResourceManager Allocate and release locomotives using SimPy Resource
TrackCapacityManager Manage track capacity and wagon placement using SimPy Container
WorkshopResourceManager Manage workshop bay availability using SimPy Resource

Workflow Sequence

sequenceDiagram
    participant Train
    participant Arrival
    participant Collection
    participant Workshop
    participant Parking

    Train->>Arrival: TrainArrivedEvent
    Arrival->>Arrival: Classify wagons
    Arrival->>Collection: Add to collection queue
    Collection->>Collection: Form batch
    Collection->>Collection: Allocate locomotive
    Collection->>Workshop: Transport to retrofit track
    Workshop->>Workshop: Select workshop
    Workshop->>Workshop: Allocate stations
    Workshop->>Workshop: Execute retrofit
    Workshop->>Parking: Move to retrofitted track
    Parking->>Parking: Form batch
    Parking->>Parking: Transport to parking

Coordinator Configuration Pattern

Each coordinator receives a configuration dataclass with all dependencies:

@dataclass
class CollectionCoordinatorConfig:
    """Configuration for CollectionCoordinator."""
    env: Any
    collection_queue: Any
    retrofit_queue: Any
    track_capacity_manager: TrackCapacityManager
    locomotive_manager: LocomotiveResourceManager
    batch_formation_service: BatchFormationService
    rake_formation_service: RakeFormationService
    train_formation_service: TrainFormationService
    route_service: RouteService
    wagon_event_publisher: Callable[[WagonLifecycleEvent], None]
    locomotive_event_publisher: Callable[[LocomotiveEvent], None]
    resource_event_publisher: Callable[[ResourceEvent], None]
    scenario: Scenario

Benefits: - Explicit dependencies - Easy testing (inject mocks) - Type-safe configuration - Clear coordinator requirements

Code Example: Coordinator Structure

class CollectionCoordinator:
    """Coordinates wagon movement from collection to retrofit track."""

    def __init__(self, config: CollectionCoordinatorConfig):
        self.config = config
        self.batch_counter = 0

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

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

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

            # Select retrofit track using domain service
            retrofit_track = self.config.track_capacity_manager.select_track('retrofit')

            # Transport batch
            yield from self._transport_batch(wagons, retrofit_track)

            # Publish event
            self.config.wagon_event_publisher(
                WagonLifecycleEvent(
                    wagon_id=wagon.id,
                    event_type="batch_transported",
                    sim_time=self.config.env.now
                )
            )

5.4 Level 2: Railway Infrastructure Context

Responsibility

Manage track capacity, occupancy, and wagon placement using domain aggregates.

graph TB
    subgraph "Railway Infrastructure Context"
        Context["RailwayContext<br/>Track building & services"]
        TrackGroup["TrackGroup<br/>Group tracks by type"]
        Track["Track<br/>Individual track entity"]
        Occupancy["TrackOccupancy<br/>Wagon placement logic"]
        Selector["TrackSelector<br/>Selection strategies"]
    end

    Context --> TrackGroup
    TrackGroup --> Track
    Track --> Occupancy
    Context --> Selector
    Selector --> TrackGroup

    classDef component fill:#e1f5fe,stroke:#01579b,stroke-width:2px

    class Context,TrackGroup,Track,Occupancy,Selector component

Components

Component Responsibility Pattern
RailwayContext Build tracks from scenario, provide services Context
TrackGroup Group tracks by type (collection, retrofit, parking, workshop) Aggregate
Track Individual track with capacity and fill factor Entity
TrackOccupancy Manage wagon placement and capacity Aggregate
TrackSelector Select tracks using strategies (round-robin, least-occupied, etc.) Service

Track Selection Strategies

Strategy Behavior
first_available Select first track with available capacity
least_occupied Select track with lowest utilization
round_robin Rotate through tracks sequentially
best_fit Select track that best fits wagon length

5.5 Level 2: External Trains Context

Responsibility

Handle train arrivals and create wagon entities as single source of truth.

graph TB
    subgraph "External Trains Context"
        Context["ExternalTrainsContext<br/>Train arrival processing"]
        Schedule["TrainSchedule<br/>Scheduled trains"]
        Train["ExternalTrain<br/>Train entity"]
        Wagon["Wagon<br/>Wagon entity"]
    end

    Scenario["Scenario.trains"] --> Context
    Context --> Schedule
    Schedule --> Train
    Train --> Wagon
    Wagon --> Event["TrainArrivedEvent"]

    classDef component fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef external fill:#f3e5f5,stroke:#4a148c,stroke-width:2px

    class Context,Schedule,Train,Wagon component
    class Scenario,Event external

Components

Component Responsibility
ExternalTrainsContext Process train arrivals, create wagons, publish events
TrainSchedule Manage scheduled train arrivals
ExternalTrain Train entity with arrival time and wagons
Wagon Wagon entity (single source of truth)

Code Example

class ExternalTrainsContext:
    """External Trains Context managing train arrivals."""

    def __init__(self, event_bus: EventBus):
        self.event_bus = event_bus
        self.scenario: Scenario | None = None
        self._wagons: dict[str, Wagon] = {}  # Single source of truth

    def start_processes(self) -> None:
        """Start train arrival processes."""
        if self.infra and self.scenario:
            for train in self.scenario.trains:
                self.infra.engine.schedule_process(
                    self._process_single_train_arrival(train)
                )

    def _process_single_train_arrival(self, train: Any) -> Any:
        """Process a single train arrival."""
        # Wait for arrival time
        arrival_delay = datetime_to_ticks(train.arrival_time, self.scenario.start_date)
        yield from self.infra.engine.delay(arrival_delay)

        # Create wagons
        train_wagons = [Wagon(...) for wagon_dto in train.wagons]

        # Store as single source of truth
        for wagon in train_wagons:
            self._wagons[wagon.id] = wagon

        # Publish event
        event = TrainArrivedEvent(
            train_id=train.train_id,
            wagons=train_wagons,
            arrival_track='collection',
            event_timestamp=self.infra.engine.current_time()
        )
        self.event_bus.publish(event)

5.6 Cross-Cutting Concerns

Event Collection System

The system uses an EventCollector for centralized event management and metrics collection.

Implementation: contexts/retrofit_workflow/application/event_collector.py

class EventCollector:
    """Centralized event collection for simulation metrics."""

    def __init__(self, start_datetime: datetime | None = None):
        self._wagon_events: list[WagonLifecycleEvent] = []
        self._locomotive_events: list[LocomotiveEvent] = []
        self._resource_events: list[ResourceEvent] = []
        self._batch_events: list[BatchEvent] = []

    def add_wagon_event(self, event: WagonLifecycleEvent) -> None:
        """Add wagon lifecycle event."""
        self._wagon_events.append(event)

    def export_wagon_journey(self, output_path: Path) -> None:
        """Export wagon events to CSV."""
        # CSV export implementation

Dual-Stream Events: - State Events: Lifecycle state changes (arrived, classified, retrofit_started, completed, parked) - Location Events: Physical movement between tracks

Event Publisher Pattern: Coordinators receive event publisher functions via configuration:

self.config.wagon_event_publisher(
    WagonLifecycleEvent(
        wagon_id=wagon.id,
        event_type="batch_formed",
        sim_time=self.config.env.now
    )
)

Key Events: - WagonLifecycleEvent - Wagon state changes (arrived, classified, retrofit_started, completed, parked) - LocomotiveEvent - Locomotive allocation and release - ResourceEvent - Resource utilization events - BatchEvent - Batch formation and transport events

Metrics Collection

The EventCollector aggregates metrics during simulation: - Wagon Journey: Complete lifecycle from arrival to parking - Locomotive Operations: Allocation, release, utilization - Batch Operations: Formation, transport, dissolution - Workshop Utilization: Bay occupancy, processing times - Track Occupancy: Capacity usage over time

Export Formats: - wagon_journey.csv - Complete wagon lifecycle events - locomotive_movements.csv - Locomotive allocation history - rejected_wagons.csv - Wagons that could not be processed - summary_metrics.json - Aggregated KPIs

Time Conversion

Shared utilities convert between datetime and SimPy simulation ticks: - datetime_to_ticks() - Convert datetime to simulation time - timedelta_to_sim_ticks() - Convert timedelta to simulation duration


5.7 Deployment Units

The system is deployed as a single Python application with CLI interface:

popupsim/
├── backend/
│   └── src/
│       ├── main.py                    # CLI entry point
│       ├── application/               # Application services
│       ├── contexts/                  # Bounded contexts
│       │   ├── configuration/
│       │   ├── retrofit_workflow/
│       │   ├── railway_infrastructure/
│       │   └── external_trains/
│       ├── shared/                    # Shared kernel
│       └── infrastructure/            # Technical infrastructure

Execution:

uv run python src/main.py --scenario path/to/scenario --output path/to/output