2. MVP Bounded Contexts¶
2.1 Context Overview¶
Note: See Architecture Section 5 for detailed building blocks.
The MVP uses 4 bounded contexts following Domain-Driven Design principles:
graph TB
subgraph "MVP PopUpSim - 4 Contexts"
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[Config Files<br/>JSON/CSV]
Output[Results<br/>CSV/PNG]
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
2.2 Configuration Context¶
Responsibilities¶
- Load scenario configuration from files
- Validate using Pydantic models
- Provide validated scenario to other contexts
Key Components¶
Location: popupsim/backend/src/contexts/configuration/
configuration/
├── domain/
│ ├── configuration_builder.py # Entry point
│ └── models/
│ ├── scenario.py
│ ├── process_times.py
│ └── dtos/
└── infrastructure/
└── file_loader.py # Parse JSON/CSV
Implementation¶
File: contexts/configuration/domain/configuration_builder.py
from pathlib import Path
from contexts.configuration.infrastructure.file_loader import FileLoader
from contexts.configuration.domain.models.scenario import Scenario
class ConfigurationBuilder:
"""Load scenario from file path."""
def __init__(self, path: Path):
self.path = path
def build(self) -> Scenario:
"""Load and validate scenario."""
loader = FileLoader()
return loader.load_scenario(self.path)
2.3 Retrofit Workflow Context¶
Responsibilities¶
- Execute discrete event simulation
- Coordinate wagon flow through 4 coordinators
- Manage resources (locomotives, tracks, workshops)
- Collect metrics during simulation
- Export results and generate reports
Key Components¶
Location: popupsim/backend/src/contexts/retrofit_workflow/
retrofit_workflow/
├── application/
│ ├── retrofit_workflow_context.py # Main context
│ └── coordinators/
│ ├── arrival_coordinator.py # Process train arrivals
│ ├── collection_coordinator.py # Move to retrofit track
│ ├── workshop_coordinator.py # Retrofit operations
│ └── parking_coordinator.py # Move to parking
├── domain/
│ └── services/
│ ├── batch_formation_service.py
│ ├── rake_formation_service.py
│ ├── train_formation_service.py
│ ├── workshop_scheduling_service.py
│ ├── coupling_service.py
│ └── route_service.py
└── infrastructure/
├── resource_managers/
│ ├── locomotive_resource_manager.py
│ ├── track_capacity_manager.py
│ └── workshop_resource_manager.py
└── metrics/
├── simulation_metrics.py
├── wagon_collector.py
├── locomotive_collector.py
└── workshop_collector.py
Implementation: Main Context¶
File: contexts/retrofit_workflow/application/retrofit_workflow_context.py
from typing import Any
from pathlib import Path
from contexts.configuration.domain.models.scenario import Scenario
from contexts.railway_infrastructure.application.railway_context import RailwayContext
from contexts.external_trains.application.external_trains_context import ExternalTrainsContext
from contexts.retrofit_workflow.application.event_collector import EventCollector
class RetrofitWorkflowContext:
"""Main retrofit workflow simulation context."""
def __init__(
self,
env: Any,
scenario: Scenario,
railway: RailwayContext,
external_trains: ExternalTrainsContext,
):
self.env = env
self.scenario = scenario
self.railway = railway
self.external_trains = external_trains
# Initialize event collector
self.event_collector = EventCollector(start_datetime=scenario.start_date)
# Initialize coordinators with event publishers
self.arrival_coordinator = ArrivalCoordinator(
config=ArrivalCoordinatorConfig(
wagon_event_publisher=self.event_collector.add_wagon_event,
# ... other config
)
)
self.collection_coordinator = CollectionCoordinator(...)
self.workshop_coordinator = WorkshopCoordinator(...)
self.parking_coordinator = ParkingCoordinator(...)
def start_processes(self) -> None:
"""Start all coordinators."""
self.arrival_coordinator.start()
self.collection_coordinator.start()
self.workshop_coordinator.start()
self.parking_coordinator.start()
def export_events(self, output_path: Path) -> None:
"""Export simulation events."""
self.event_collector.export_all(output_path)
Implementation: Coordinator Example¶
File: contexts/retrofit_workflow/application/coordinators/collection_coordinator.py
from typing import Generator, Any
from dataclasses import dataclass
from collections.abc import Callable
@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]
scenario: Scenario
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)
# Publish event
self.config.wagon_event_publisher(
WagonLifecycleEvent(
wagon_id=wagon.id,
event_type="batch_formed",
sim_time=self.config.env.now
)
)
# Transport batch
yield from self._transport_batch(wagons)
Implementation: Domain Service Example¶
File: contexts/retrofit_workflow/domain/services/batch_formation_service.py
class BatchFormationService:
"""Form wagon batches (no SimPy dependencies)."""
@staticmethod
def can_form_batch(
wagons: list[Wagon],
min_batch_size: int,
max_batch_size: int,
) -> bool:
"""Check if batch can be formed."""
return min_batch_size <= len(wagons) <= max_batch_size
@staticmethod
def form_batch(
wagons: list[Wagon],
batch_size: int,
) -> list[Wagon]:
"""Form batch of specified size."""
return wagons[:batch_size]
2.4 Railway Infrastructure Context¶
Responsibilities¶
- Build track infrastructure from scenario
- Manage track capacity and occupancy
- Provide track selection services
- Track wagon placement
Key Components¶
Location: popupsim/backend/src/contexts/railway_infrastructure/
railway_infrastructure/
├── application/
│ └── railway_context.py # Track building & services
├── domain/
│ ├── aggregates/
│ │ ├── track_group.py
│ │ ├── track.py
│ │ └── track_occupancy.py
│ └── services/
│ ├── track_selector.py
│ └── capacity_service.py
└── infrastructure/
└── track_repository.py
Implementation¶
File: contexts/railway_infrastructure/application/railway_context.py
from contexts.configuration.domain.models.scenario import Scenario
from contexts.railway_infrastructure.domain.aggregates.track_group import TrackGroup
from contexts.railway_infrastructure.domain.services.track_selector import TrackSelector
class RailwayContext:
"""Railway infrastructure management context."""
def __init__(self, scenario: Scenario):
self.scenario = scenario
# Build track groups
self.track_groups = self._build_track_groups(scenario.tracks)
# Initialize services
self.track_selector = TrackSelector(self.track_groups)
self.capacity_service = CapacityService(self.track_groups)
def _build_track_groups(self, tracks: list[Track]) -> dict[str, TrackGroup]:
"""Group tracks by type."""
groups = {}
for track in tracks:
if track.track_type not in groups:
groups[track.track_type] = TrackGroup(track.track_type)
groups[track.track_type].add_track(track)
return groups
def place_wagons_on_track(self, track_id: str, wagons: list[Wagon]) -> None:
"""Place wagons on specified track."""
track = self._find_track(track_id)
track.occupancy.add_wagons(wagons)
def remove_wagons_from_track(self, track_id: str, wagons: list[Wagon]) -> None:
"""Remove wagons from specified track."""
track = self._find_track(track_id)
track.occupancy.remove_wagons(wagons)
2.5 External Trains Context¶
Responsibilities¶
- Initialize train arrivals from scenario
- Publish TrainArrivedEvent to event bus
- Create wagon entities from train data
Key Components¶
Location: popupsim/backend/src/contexts/external_trains/
external_trains/
├── application/
│ └── external_trains_context.py # Train arrival management
├── domain/
│ ├── wagon_factory.py
│ └── events/
│ └── train_arrived_event.py
└── infrastructure/
└── event_publisher.py
Implementation¶
File: contexts/external_trains/application/external_trains_context.py
from contexts.configuration.domain.models.scenario import Scenario
from contexts.shared.domain.events.event_bus import EventBus
from contexts.external_trains.domain.events.train_arrived_event import TrainArrivedEvent
class ExternalTrainsContext:
"""External train arrival management."""
def __init__(self, env: Any, scenario: Scenario, event_bus: EventBus):
self.env = env
self.scenario = scenario
self.event_bus = event_bus
def initialize_arrivals(self) -> None:
"""Schedule all train arrivals."""
for train in self.scenario.trains or []:
arrival_time = self._calculate_arrival_time(train)
self.env.process(self._arrival_process(train, arrival_time))
def _arrival_process(self, train: Train, arrival_time: float) -> Generator[Any, Any, None]:
"""Process single train arrival."""
yield self.env.timeout(arrival_time)
# Create event
event = TrainArrivedEvent(
train_id=train.id,
wagons=train.wagons,
arrival_time=self.env.now,
)
# Publish to event bus
self.event_bus.publish(event)
2.6 Context Interactions¶
Main Orchestration¶
File: popupsim/backend/src/main.py
from pathlib import Path
from contexts.configuration.infrastructure.file_loader import FileLoader
from contexts.railway_infrastructure.application.railway_context import RailwayContext
from contexts.external_trains.application.external_trains_context import ExternalTrainsContext
from contexts.retrofit_workflow.application.retrofit_workflow_context import RetrofitWorkflowContext
import simpy
def run_simulation(scenario_path: Path, output_path: Path) -> None:
"""Run complete simulation pipeline."""
# 1. Configuration Context - Load scenario
loader = FileLoader()
scenario = loader.load_scenario(scenario_path)
# 2. Initialize SimPy environment
env = simpy.Environment()
# 3. Railway Infrastructure Context - Build tracks
railway = RailwayContext(scenario)
# 4. External Trains Context - Schedule arrivals
external_trains = ExternalTrainsContext(env, scenario)
# 5. Retrofit Workflow Context - Main simulation
retrofit_workflow = RetrofitWorkflowContext(
env, scenario, railway, external_trains
)
# 6. Initialize and start
retrofit_workflow.initialize()
external_trains.start_processes()
retrofit_workflow.start_processes()
# 7. Run simulation
env.run()
# 8. Export results
retrofit_workflow.export_events(output_path)
Event Collection Pattern¶
# EventCollector is initialized in RetrofitWorkflowContext
event_collector = EventCollector(start_datetime=scenario.start_date)
# Event publishers are passed to coordinators via config
coordinator_config = CollectionCoordinatorConfig(
wagon_event_publisher=event_collector.add_wagon_event,
locomotive_event_publisher=event_collector.add_locomotive_event,
# ... other config
)
# Coordinators publish events
self.config.wagon_event_publisher(
WagonLifecycleEvent(
wagon_id=wagon.id,
event_type="batch_formed",
sim_time=self.config.env.now
)
)
# Export at end of simulation
event_collector.export_all(output_path)
2.7 Implementation Status¶
| Context | Status | Location |
|---|---|---|
| Configuration Context | ✅ Implemented | contexts/configuration/ |
| Retrofit Workflow Context | ✅ Implemented | contexts/retrofit_workflow/ |
| Railway Infrastructure Context | ✅ Implemented | contexts/railway_infrastructure/ |
| External Trains Context | ✅ Implemented | contexts/external_trains/ |
| Event Collection | ✅ Implemented | contexts/retrofit_workflow/application/event_collector.py |
| SimPy Integration | ✅ Implemented | shared/infrastructure/simulation/ |
| Testing | ✅ Complete | 378 tests passing, 54% coverage |
2.8 Migration Path to Full Version¶
The 4-context architecture provides a solid foundation for future evolution. Possible enhancements:
- Event-driven architecture: Replace direct calls with async messaging
- Database integration: Add repository pattern for persistence
- Web interface: Add API layer and frontend
- Distributed simulation: Split contexts into microservices
Current architecture supports these migrations through: - Clear context boundaries - Event bus infrastructure - Domain services without SimPy dependencies - Layered architecture within each context