3. MVP Domain Model¶
3.1 Domain Model Overview¶
Note: See actual implementation in popupsim/backend/src/contexts/
The MVP domain model follows Domain-Driven Design principles across 4 bounded contexts.
classDiagram
class ScenarioConfig {
+str scenario_id
+date start_date
+date end_date
+Workshop workshop
+str train_schedule_file
}
class Workshop {
+list tracks
+validate_tracks()
}
class WorkshopTrack {
+str id
+TrackFunction function
+int retrofit_time_min
}
class Train {
+str id
+datetime arrival_time
+list wagons
+get_total_length() float
}
class Wagon {
+str id
+str train_id
+float length
+bool needs_retrofit
+str status
}
ScenarioConfig --o Workshop
Workshop --o WorkshopTrack
Train --o Wagon
3.2 Configuration Context Models¶
Actual implementation: popupsim/backend/src/contexts/configuration/domain/models/
Scenario¶
from pydantic import BaseModel
from datetime import datetime
class Scenario(BaseModel):
"""Root configuration model."""
id: str
start_date: datetime
end_date: datetime
trains: list[Train] | None = None
tracks: list[Track] | None = None
workshops: list[Workshop] | None = None
locomotives: list[Locomotive] | None = None
routes: list[Route] | None = None
process_times: ProcessTimes | None = None
Workshop¶
class Workshop(BaseModel):
"""Workshop configuration."""
workshop_id: str
track_id: str
retrofit_stations: int
name: str | None = None
3.3 Retrofit Workflow Domain Services¶
Actual implementation: popupsim/backend/src/contexts/retrofit_workflow/domain/services/
Batch Formation Service¶
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
Workshop Scheduling Service¶
class WorkshopSchedulingService:
"""Schedule wagon batches to workshops (no SimPy dependencies)."""
@staticmethod
def select_workshop(
workshops: list[Workshop],
batch_size: int,
) -> Workshop | None:
"""Select workshop with sufficient capacity."""
for workshop in workshops:
if workshop.retrofit_stations >= batch_size:
return workshop
return None
3.4 Value Objects¶
from dataclasses import dataclass
@dataclass(frozen=True)
class ValidationResult:
"""Result of validation"""
is_valid: bool
errors: list[str]
warnings: list[str]
def has_errors(self) -> bool:
return len(self.errors) > 0
@dataclass(frozen=True)
class SimulationResult:
"""Result of simulation"""
scenario_id: str
duration_hours: int
total_trains_processed: int
total_wagons_processed: int
average_processing_time_minutes: float
throughput_per_hour: float
3.5 Domain Services¶
class ThroughputCalculationService:
"""Service for throughput calculations"""
def calculate_theoretical_throughput(
self,
workshop: Workshop
) -> float:
"""Calculates theoretical workshop throughput"""
total_capacity = sum(t.capacity for t in workshop.tracks)
avg_processing_time = sum(
t.retrofit_time_min for t in workshop.tracks
) / len(workshop.tracks)
wagons_per_hour = (total_capacity * 60) / avg_processing_time
return wagons_per_hour
class ValidationService:
"""Service for data validation"""
def validate_scenario(
self,
config: ScenarioConfig
) -> ValidationResult:
"""Validates scenario models"""
errors = []
warnings = []
# Date validation
if config.end_date <= config.start_date:
errors.append("end_date must be after start_date")
# Workshop validation
if config.workshop and not config.workshop.tracks:
errors.append("Workshop must have at least one track")
return ValidationResult(
is_valid=len(errors) == 0,
errors=errors,
warnings=warnings
)
3.6 Exceptions¶
class PopUpSimDomainError(Exception):
"""Base for domain-specific errors"""
pass
class ValidationError(PopUpSimDomainError):
"""Configuration validation error"""
pass
class SimulationRuntimeError(PopUpSimDomainError):
"""Error during simulation"""
pass
class InsufficientCapacityError(PopUpSimDomainError):
"""Insufficient track/workshop capacity"""
pass
3.7 Type Hints¶
All code must include explicit type annotations per project rules:
from typing import Optional
def process_wagon(
wagon: Wagon,
track: WorkshopTrack
) -> Optional[float]:
"""Process wagon on track, returns completion time"""
if not track.capacity > 0:
raise InsufficientCapacityError(f"Track {track.id} is full")
completion_time = track.retrofit_time_min
return completion_time
def validate_configuration(
config: ScenarioConfig
) -> ValidationResult:
"""Validate scenario models"""
# Implementation
pass
3.8 Migration Path¶
The simplified MVP domain model can be extended to full DDD implementation:
Phase 1 (Post-MVP): Rich Domain Model¶
- Aggregate roots with invariants
- Domain services for complex business logic
- Repository pattern for persistence
Phase 2: Event Sourcing¶
- Event store implementation
- Event-driven state reconstruction
- Temporal queries
Phase 3: Advanced DDD¶
- Specification pattern for complex queries
- Domain events with saga pattern
- CQRS for read/write separation
Effort: Estimated 2-3 weeks for full DDD migration (to be validated)