Commit ·
a8ba5ce
1
Parent(s): 6f28b30
protoype integration completed
Browse files- DataService/README.md +391 -0
- DataService/__init__.py +29 -0
- DataService/__pycache__/__init__.cpython-312.pyc +0 -0
- DataService/__pycache__/metro_data_generator.cpython-312.pyc +0 -0
- DataService/__pycache__/metro_models.cpython-312.pyc +0 -0
- DataService/__pycache__/schedule_optimizer.cpython-312.pyc +0 -0
- DataService/api.py +275 -0
- DataService/metro_data_generator.py +233 -0
- DataService/metro_models.py +232 -0
- DataService/schedule_optimizer.py +405 -0
- demo_schedule.py +262 -0
- quickstart.py +257 -0
- requirements.txt +5 -1
- run_api.py +37 -0
- test_system.py +214 -0
DataService/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Metro Train Scheduling System - DataService API
|
| 2 |
+
|
| 3 |
+
A comprehensive FastAPI-based service for generating synthetic metro train scheduling data and optimizing daily train operations.
|
| 4 |
+
|
| 5 |
+
## 🎯 Overview
|
| 6 |
+
|
| 7 |
+
This system generates realistic metro train schedules for a single-line metro network with:
|
| 8 |
+
- **25-40 trainsets** with varying health status
|
| 9 |
+
- **25 stations** on a bidirectional route
|
| 10 |
+
- **Operating hours**: 5:00 AM - 11:00 PM
|
| 11 |
+
- **Real-world constraints**: maintenance, fitness certificates, branding priorities, mileage balancing
|
| 12 |
+
|
| 13 |
+
## 🚇 Features
|
| 14 |
+
|
| 15 |
+
### Data Generation
|
| 16 |
+
- **Train Health Status**: Fully healthy, partially available, or under maintenance
|
| 17 |
+
- **Fitness Certificates**: Rolling stock, signalling, and telecom certificates with expiry tracking
|
| 18 |
+
- **Job Cards**: Open maintenance tasks with blocking status
|
| 19 |
+
- **Component Health**: IoT-style monitoring of brakes, HVAC, doors, bogies, etc.
|
| 20 |
+
- **Branding/Advertising**: Contract tracking with exposure priorities
|
| 21 |
+
- **Depot Layout**: Stabling bays, IBL bays, and washing bays
|
| 22 |
+
|
| 23 |
+
### Schedule Optimization
|
| 24 |
+
- **Multi-objective optimization** balancing:
|
| 25 |
+
- Service readiness (35%)
|
| 26 |
+
- Mileage balancing (25%)
|
| 27 |
+
- Branding priority (20%)
|
| 28 |
+
- Operational cost (20%)
|
| 29 |
+
- **Constraint satisfaction**: Fitness certificates, maintenance requirements, availability windows
|
| 30 |
+
- **Service block generation**: Optimal trip assignments throughout the day
|
| 31 |
+
- **Fleet allocation**: Revenue service, standby, maintenance, and cleaning assignments
|
| 32 |
+
|
| 33 |
+
### API Endpoints
|
| 34 |
+
|
| 35 |
+
#### Generate Complete Schedule
|
| 36 |
+
```bash
|
| 37 |
+
POST /api/v1/generate
|
| 38 |
+
Content-Type: application/json
|
| 39 |
+
|
| 40 |
+
{
|
| 41 |
+
"date": "2025-10-25",
|
| 42 |
+
"num_trains": 30,
|
| 43 |
+
"num_stations": 25,
|
| 44 |
+
"route_name": "Aluva-Pettah Line",
|
| 45 |
+
"depot_name": "Muttom_Depot",
|
| 46 |
+
"min_service_trains": 22,
|
| 47 |
+
"min_standby_trains": 3,
|
| 48 |
+
"max_daily_km_per_train": 300,
|
| 49 |
+
"balance_mileage": true,
|
| 50 |
+
"prioritize_branding": true
|
| 51 |
+
}
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
#### Quick Schedule Generation
|
| 55 |
+
```bash
|
| 56 |
+
POST /api/v1/generate/quick?date=2025-10-25&num_trains=30&num_stations=25
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
#### Get Example Schedule
|
| 60 |
+
```bash
|
| 61 |
+
GET /api/v1/schedule/example
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
#### Get Route Information
|
| 65 |
+
```bash
|
| 66 |
+
GET /api/v1/route/25
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
#### Get Train Health Status
|
| 70 |
+
```bash
|
| 71 |
+
GET /api/v1/trains/health/30
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
#### Get Depot Layout
|
| 75 |
+
```bash
|
| 76 |
+
GET /api/v1/depot/layout
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## 📦 Installation
|
| 80 |
+
|
| 81 |
+
### Prerequisites
|
| 82 |
+
- Python 3.8+
|
| 83 |
+
- pip
|
| 84 |
+
|
| 85 |
+
### Setup
|
| 86 |
+
|
| 87 |
+
1. **Clone the repository**
|
| 88 |
+
```bash
|
| 89 |
+
cd /home/arpbansal/code/sih2025/mlservice
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
2. **Install dependencies**
|
| 93 |
+
```bash
|
| 94 |
+
pip install -r requirements.txt
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Requirements include:
|
| 98 |
+
- `fastapi>=0.104.1` - Web framework
|
| 99 |
+
- `uvicorn[standard]>=0.24.0` - ASGI server
|
| 100 |
+
- `pydantic>=2.5.0` - Data validation
|
| 101 |
+
- `ortools>=9.14.6206` - Optimization (optional)
|
| 102 |
+
|
| 103 |
+
## 🚀 Usage
|
| 104 |
+
|
| 105 |
+
### Option 1: Run Demo Script
|
| 106 |
+
|
| 107 |
+
Test the system without starting the API:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
python demo_schedule.py
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
This will:
|
| 114 |
+
- Generate synthetic metro data
|
| 115 |
+
- Optimize a daily schedule
|
| 116 |
+
- Display comprehensive results
|
| 117 |
+
- Save output to `sample_schedule.json`
|
| 118 |
+
|
| 119 |
+
### Option 2: Start FastAPI Server
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
python run_api.py
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
The API will be available at:
|
| 126 |
+
- **Base URL**: http://localhost:8000
|
| 127 |
+
- **Interactive Docs**: http://localhost:8000/docs
|
| 128 |
+
- **Alternative Docs**: http://localhost:8000/redoc
|
| 129 |
+
|
| 130 |
+
### Option 3: Use uvicorn directly
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
uvicorn DataService.api:app --reload --host 0.0.0.0 --port 8000
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## 📊 Schedule Output Structure
|
| 137 |
+
|
| 138 |
+
```json
|
| 139 |
+
{
|
| 140 |
+
"schedule_id": "KMRL-2025-10-25-DAWN",
|
| 141 |
+
"generated_at": "2025-10-24T23:45:00+05:30",
|
| 142 |
+
"valid_from": "2025-10-25T05:00:00+05:30",
|
| 143 |
+
"valid_until": "2025-10-25T23:00:00+05:30",
|
| 144 |
+
"depot": "Muttom_Depot",
|
| 145 |
+
|
| 146 |
+
"trainsets": [
|
| 147 |
+
{
|
| 148 |
+
"trainset_id": "TS-001",
|
| 149 |
+
"status": "REVENUE_SERVICE",
|
| 150 |
+
"priority_rank": 1,
|
| 151 |
+
"assigned_duty": "DUTY-A1",
|
| 152 |
+
"service_blocks": [
|
| 153 |
+
{
|
| 154 |
+
"block_id": "BLK-001",
|
| 155 |
+
"departure_time": "05:30",
|
| 156 |
+
"origin": "Aluva",
|
| 157 |
+
"destination": "Pettah",
|
| 158 |
+
"trip_count": 3,
|
| 159 |
+
"estimated_km": 96
|
| 160 |
+
}
|
| 161 |
+
],
|
| 162 |
+
"daily_km_allocation": 224,
|
| 163 |
+
"cumulative_km": 145620,
|
| 164 |
+
"fitness_certificates": {
|
| 165 |
+
"rolling_stock": {"valid_until": "2025-11-15", "status": "VALID"},
|
| 166 |
+
"signalling": {"valid_until": "2025-10-30", "status": "VALID"},
|
| 167 |
+
"telecom": {"valid_until": "2025-11-20", "status": "VALID"}
|
| 168 |
+
},
|
| 169 |
+
"job_cards": {"open": 0, "blocking": []},
|
| 170 |
+
"branding": {
|
| 171 |
+
"advertiser": "COCACOLA-2024",
|
| 172 |
+
"contract_hours_remaining": 340,
|
| 173 |
+
"exposure_priority": "HIGH"
|
| 174 |
+
},
|
| 175 |
+
"readiness_score": 0.98,
|
| 176 |
+
"constraints_met": true
|
| 177 |
+
}
|
| 178 |
+
],
|
| 179 |
+
|
| 180 |
+
"fleet_summary": {
|
| 181 |
+
"total_trainsets": 30,
|
| 182 |
+
"revenue_service": 22,
|
| 183 |
+
"standby": 4,
|
| 184 |
+
"maintenance": 2,
|
| 185 |
+
"cleaning": 2,
|
| 186 |
+
"availability_percent": 93.3
|
| 187 |
+
},
|
| 188 |
+
|
| 189 |
+
"optimization_metrics": {
|
| 190 |
+
"mileage_variance_coefficient": 0.042,
|
| 191 |
+
"avg_readiness_score": 0.91,
|
| 192 |
+
"branding_sla_compliance": 1.0,
|
| 193 |
+
"shunting_movements_required": 8,
|
| 194 |
+
"total_planned_km": 5280,
|
| 195 |
+
"fitness_expiry_violations": 0
|
| 196 |
+
},
|
| 197 |
+
|
| 198 |
+
"conflicts_and_alerts": [...],
|
| 199 |
+
"decision_rationale": {...}
|
| 200 |
+
}
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
## 🏗️ Architecture
|
| 204 |
+
|
| 205 |
+
```
|
| 206 |
+
mlservice/
|
| 207 |
+
├── DataService/
|
| 208 |
+
│ ├── __init__.py
|
| 209 |
+
│ ├── api.py # FastAPI application
|
| 210 |
+
│ ├── metro_models.py # Pydantic data models
|
| 211 |
+
│ ├── metro_data_generator.py # Synthetic data generation
|
| 212 |
+
│ ├── schedule_optimizer.py # Schedule optimization logic
|
| 213 |
+
│ ├── enhanced_generator.py # (existing)
|
| 214 |
+
│ ├── synthetic_base.py # (existing)
|
| 215 |
+
│ └── synthetic_extend.py # (existing)
|
| 216 |
+
├── greedyOptim/ # Optimization algorithms
|
| 217 |
+
├── demo_schedule.py # Demo/test script
|
| 218 |
+
├── run_api.py # API startup script
|
| 219 |
+
├── requirements.txt
|
| 220 |
+
└── README.md
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
## 🔧 Configuration
|
| 224 |
+
|
| 225 |
+
### Train Health Categories
|
| 226 |
+
|
| 227 |
+
- **Fully Healthy** (65%): Available entire operational day
|
| 228 |
+
- **Partially Healthy** (20%): Available for limited hours
|
| 229 |
+
- **Unavailable** (15%): Not available for service (maintenance/repairs)
|
| 230 |
+
|
| 231 |
+
### Train Status Types
|
| 232 |
+
|
| 233 |
+
- `REVENUE_SERVICE`: Active passenger service
|
| 234 |
+
- `STANDBY`: Ready for deployment
|
| 235 |
+
- `MAINTENANCE`: Under repair/inspection
|
| 236 |
+
- `CLEANING`: Washing/interior cleaning
|
| 237 |
+
- `OUT_OF_SERVICE`: Long-term unavailable
|
| 238 |
+
|
| 239 |
+
### Optimization Weights
|
| 240 |
+
|
| 241 |
+
Default objective weights (configurable):
|
| 242 |
+
```python
|
| 243 |
+
{
|
| 244 |
+
"service_readiness": 0.35,
|
| 245 |
+
"mileage_balancing": 0.25,
|
| 246 |
+
"branding_priority": 0.20,
|
| 247 |
+
"operational_cost": 0.20
|
| 248 |
+
}
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
## 📝 API Examples
|
| 252 |
+
|
| 253 |
+
### cURL Examples
|
| 254 |
+
|
| 255 |
+
**Generate schedule:**
|
| 256 |
+
```bash
|
| 257 |
+
curl -X POST "http://localhost:8000/api/v1/generate" \
|
| 258 |
+
-H "Content-Type: application/json" \
|
| 259 |
+
-d '{
|
| 260 |
+
"date": "2025-10-25",
|
| 261 |
+
"num_trains": 30,
|
| 262 |
+
"num_stations": 25,
|
| 263 |
+
"min_service_trains": 22
|
| 264 |
+
}'
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
**Quick generation:**
|
| 268 |
+
```bash
|
| 269 |
+
curl "http://localhost:8000/api/v1/generate/quick?date=2025-10-25&num_trains=30"
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
**Health check:**
|
| 273 |
+
```bash
|
| 274 |
+
curl "http://localhost:8000/health"
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
### Python Client Example
|
| 278 |
+
|
| 279 |
+
```python
|
| 280 |
+
import requests
|
| 281 |
+
|
| 282 |
+
# Generate schedule
|
| 283 |
+
response = requests.post(
|
| 284 |
+
"http://localhost:8000/api/v1/generate",
|
| 285 |
+
json={
|
| 286 |
+
"date": "2025-10-25",
|
| 287 |
+
"num_trains": 30,
|
| 288 |
+
"num_stations": 25,
|
| 289 |
+
"min_service_trains": 22,
|
| 290 |
+
"min_standby_trains": 3
|
| 291 |
+
}
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
schedule = response.json()
|
| 295 |
+
print(f"Schedule ID: {schedule['schedule_id']}")
|
| 296 |
+
print(f"Trains in service: {schedule['fleet_summary']['revenue_service']}")
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
## 🧪 Testing
|
| 300 |
+
|
| 301 |
+
Run the demo script to test all functionality:
|
| 302 |
+
|
| 303 |
+
```bash
|
| 304 |
+
python demo_schedule.py
|
| 305 |
+
```
|
| 306 |
+
|
| 307 |
+
Expected output:
|
| 308 |
+
- ✓ Data generation statistics
|
| 309 |
+
- ✓ Route information
|
| 310 |
+
- ✓ Train health summary
|
| 311 |
+
- ✓ Optimization results
|
| 312 |
+
- ✓ Fleet status breakdown
|
| 313 |
+
- ✓ Sample train details
|
| 314 |
+
- ✓ JSON export
|
| 315 |
+
|
| 316 |
+
## 🎨 Key Concepts
|
| 317 |
+
|
| 318 |
+
### Service Blocks
|
| 319 |
+
Continuous operating periods with specific origin/destination and trip counts. Each block represents a trainset's assignment for part of the day.
|
| 320 |
+
|
| 321 |
+
### Readiness Score
|
| 322 |
+
Computed metric (0.0-1.0) considering:
|
| 323 |
+
- Fitness certificate validity
|
| 324 |
+
- Open/blocking job cards
|
| 325 |
+
- Component health scores
|
| 326 |
+
- Days since maintenance
|
| 327 |
+
|
| 328 |
+
### Mileage Balancing
|
| 329 |
+
Distributes daily kilometers to equalize cumulative mileage across the fleet, extending overall fleet life.
|
| 330 |
+
|
| 331 |
+
### Branding Priority
|
| 332 |
+
Trains with active advertising contracts get preferential assignment to maximize exposure (revenue optimization).
|
| 333 |
+
|
| 334 |
+
## 🔍 Monitoring & Alerts
|
| 335 |
+
|
| 336 |
+
The system generates alerts for:
|
| 337 |
+
- Certificate expirations (EXPIRING_SOON, EXPIRED)
|
| 338 |
+
- Blocking maintenance (job cards preventing service)
|
| 339 |
+
- Fitness violations
|
| 340 |
+
- Constraint conflicts
|
| 341 |
+
|
| 342 |
+
## 🤝 Integration
|
| 343 |
+
|
| 344 |
+
### With Existing Optimizer
|
| 345 |
+
|
| 346 |
+
The DataService can integrate with existing `greedyOptim` algorithms:
|
| 347 |
+
|
| 348 |
+
```python
|
| 349 |
+
from greedyOptim.scheduler import TrainsetSchedulingOptimizer
|
| 350 |
+
from DataService.metro_data_generator import MetroDataGenerator
|
| 351 |
+
|
| 352 |
+
# Generate synthetic data
|
| 353 |
+
generator = MetroDataGenerator(num_trains=30)
|
| 354 |
+
# ... use with existing optimizer
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
### As Microservice
|
| 358 |
+
|
| 359 |
+
Deploy as standalone microservice:
|
| 360 |
+
```bash
|
| 361 |
+
docker build -t metro-scheduler .
|
| 362 |
+
docker run -p 8000:8000 metro-scheduler
|
| 363 |
+
```
|
| 364 |
+
|
| 365 |
+
## 📈 Future Enhancements
|
| 366 |
+
|
| 367 |
+
- [ ] Real-time schedule adjustments
|
| 368 |
+
- [ ] Machine learning for demand prediction
|
| 369 |
+
- [ ] Driver/crew scheduling integration
|
| 370 |
+
- [ ] Energy consumption optimization
|
| 371 |
+
- [ ] Passenger flow simulation
|
| 372 |
+
- [ ] Weather impact modeling
|
| 373 |
+
- [ ] Multi-line network support
|
| 374 |
+
|
| 375 |
+
## 📄 License
|
| 376 |
+
|
| 377 |
+
[Add your license information]
|
| 378 |
+
|
| 379 |
+
## 👥 Contributors
|
| 380 |
+
|
| 381 |
+
[Add contributor information]
|
| 382 |
+
|
| 383 |
+
## 📞 Support
|
| 384 |
+
|
| 385 |
+
For issues, questions, or contributions:
|
| 386 |
+
- GitHub Issues: [repository URL]
|
| 387 |
+
- Email: [contact email]
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
**Built for Smart India Hackathon 2025** 🇮🇳
|
DataService/__init__.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DataService - Metro Train Scheduling Data Generation and API
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .metro_models import (
|
| 6 |
+
DaySchedule, Trainset, TrainStatus, ServiceBlock,
|
| 7 |
+
ScheduleRequest, Route, Station, TrainHealthStatus,
|
| 8 |
+
FitnessCertificates, JobCards, Branding
|
| 9 |
+
)
|
| 10 |
+
from .metro_data_generator import MetroDataGenerator
|
| 11 |
+
from .schedule_optimizer import MetroScheduleOptimizer
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
'DaySchedule',
|
| 15 |
+
'Trainset',
|
| 16 |
+
'TrainStatus',
|
| 17 |
+
'ServiceBlock',
|
| 18 |
+
'ScheduleRequest',
|
| 19 |
+
'Route',
|
| 20 |
+
'Station',
|
| 21 |
+
'TrainHealthStatus',
|
| 22 |
+
'FitnessCertificates',
|
| 23 |
+
'JobCards',
|
| 24 |
+
'Branding',
|
| 25 |
+
'MetroDataGenerator',
|
| 26 |
+
'MetroScheduleOptimizer',
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
__version__ = '1.0.0'
|
DataService/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (783 Bytes). View file
|
|
|
DataService/__pycache__/metro_data_generator.cpython-312.pyc
ADDED
|
Binary file (12.5 kB). View file
|
|
|
DataService/__pycache__/metro_models.cpython-312.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
DataService/__pycache__/schedule_optimizer.cpython-312.pyc
ADDED
|
Binary file (18.3 kB). View file
|
|
|
DataService/api.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI Service for Metro Train Schedule Generation
|
| 3 |
+
Provides endpoints for synthetic data generation and schedule optimization
|
| 4 |
+
"""
|
| 5 |
+
from fastapi import FastAPI, HTTPException
|
| 6 |
+
from fastapi.responses import JSONResponse
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from pydantic import ValidationError
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
from .metro_models import (
|
| 13 |
+
DaySchedule, ScheduleRequest, Route, TrainHealthStatus
|
| 14 |
+
)
|
| 15 |
+
from .metro_data_generator import MetroDataGenerator
|
| 16 |
+
from .schedule_optimizer import MetroScheduleOptimizer
|
| 17 |
+
|
| 18 |
+
# Configure logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
# Create FastAPI app
|
| 23 |
+
app = FastAPI(
|
| 24 |
+
title="Metro Train Scheduling API",
|
| 25 |
+
description="Generate synthetic metro data and optimize daily train schedules",
|
| 26 |
+
version="1.0.0",
|
| 27 |
+
docs_url="/docs",
|
| 28 |
+
redoc_url="/redoc"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Add CORS middleware
|
| 32 |
+
app.add_middleware(
|
| 33 |
+
CORSMiddleware,
|
| 34 |
+
allow_origins=["*"], # Configure appropriately for production
|
| 35 |
+
allow_credentials=True,
|
| 36 |
+
allow_methods=["*"],
|
| 37 |
+
allow_headers=["*"],
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@app.get("/")
|
| 42 |
+
async def root():
|
| 43 |
+
"""Root endpoint with API information"""
|
| 44 |
+
return {
|
| 45 |
+
"service": "Metro Train Scheduling API",
|
| 46 |
+
"version": "1.0.0",
|
| 47 |
+
"endpoints": {
|
| 48 |
+
"schedule": "/api/v1/schedule",
|
| 49 |
+
"generate": "/api/v1/generate",
|
| 50 |
+
"health": "/health",
|
| 51 |
+
"docs": "/docs"
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@app.get("/health")
|
| 57 |
+
async def health_check():
|
| 58 |
+
"""Health check endpoint"""
|
| 59 |
+
return {
|
| 60 |
+
"status": "healthy",
|
| 61 |
+
"timestamp": datetime.now().isoformat(),
|
| 62 |
+
"service": "metro-scheduling-api"
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@app.post("/api/v1/generate", response_model=DaySchedule)
|
| 67 |
+
async def generate_schedule(request: ScheduleRequest):
|
| 68 |
+
"""
|
| 69 |
+
Generate optimized daily train schedule
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
request: Schedule request with date, train count, and optimization parameters
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
DaySchedule: Complete optimized schedule with all trainset assignments
|
| 76 |
+
|
| 77 |
+
Example:
|
| 78 |
+
POST /api/v1/generate
|
| 79 |
+
{
|
| 80 |
+
"date": "2025-10-25",
|
| 81 |
+
"num_trains": 30,
|
| 82 |
+
"num_stations": 25,
|
| 83 |
+
"min_service_trains": 22,
|
| 84 |
+
"min_standby_trains": 3
|
| 85 |
+
}
|
| 86 |
+
"""
|
| 87 |
+
try:
|
| 88 |
+
logger.info(f"Generating schedule for {request.date} with {request.num_trains} trains")
|
| 89 |
+
|
| 90 |
+
# Initialize data generator
|
| 91 |
+
generator = MetroDataGenerator(
|
| 92 |
+
num_trains=request.num_trains,
|
| 93 |
+
num_stations=request.num_stations
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Generate route
|
| 97 |
+
route = generator.generate_route(request.route_name)
|
| 98 |
+
logger.info(f"Generated route: {route.name} with {len(route.stations)} stations")
|
| 99 |
+
|
| 100 |
+
# Generate or use provided train health data
|
| 101 |
+
if request.train_health_overrides:
|
| 102 |
+
train_health = request.train_health_overrides
|
| 103 |
+
else:
|
| 104 |
+
train_health = generator.generate_train_health_statuses()
|
| 105 |
+
|
| 106 |
+
logger.info(f"Train health data: {len(train_health)} trains initialized")
|
| 107 |
+
|
| 108 |
+
# Initialize optimizer
|
| 109 |
+
optimizer = MetroScheduleOptimizer(
|
| 110 |
+
date=request.date,
|
| 111 |
+
num_trains=request.num_trains,
|
| 112 |
+
route=route,
|
| 113 |
+
train_health=train_health,
|
| 114 |
+
depot_name=request.depot_name
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# Optimize schedule
|
| 118 |
+
schedule = optimizer.optimize_schedule(
|
| 119 |
+
min_service_trains=request.min_service_trains,
|
| 120 |
+
min_standby=request.min_standby_trains,
|
| 121 |
+
max_daily_km=request.max_daily_km_per_train
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
logger.info(
|
| 125 |
+
f"Schedule generated: {schedule.schedule_id}, "
|
| 126 |
+
f"{schedule.fleet_summary.revenue_service} trains in service, "
|
| 127 |
+
f"{schedule.optimization_metrics.total_planned_km} km planned"
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
return schedule
|
| 131 |
+
|
| 132 |
+
except ValidationError as e:
|
| 133 |
+
logger.error(f"Validation error: {e}")
|
| 134 |
+
raise HTTPException(status_code=422, detail=str(e))
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.error(f"Error generating schedule: {e}", exc_info=True)
|
| 137 |
+
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@app.post("/api/v1/generate/quick")
|
| 141 |
+
async def generate_quick_schedule(
|
| 142 |
+
date: str = "2025-10-25",
|
| 143 |
+
num_trains: int = 25,
|
| 144 |
+
num_stations: int = 25
|
| 145 |
+
):
|
| 146 |
+
"""
|
| 147 |
+
Quick schedule generation with default parameters
|
| 148 |
+
|
| 149 |
+
Query Parameters:
|
| 150 |
+
- date: Schedule date (YYYY-MM-DD)
|
| 151 |
+
- num_trains: Number of trains in fleet (default: 25)
|
| 152 |
+
- num_stations: Number of stations on route (default: 25)
|
| 153 |
+
"""
|
| 154 |
+
request = ScheduleRequest(
|
| 155 |
+
date=date,
|
| 156 |
+
num_trains=num_trains,
|
| 157 |
+
num_stations=num_stations
|
| 158 |
+
)
|
| 159 |
+
return await generate_schedule(request)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.get("/api/v1/route/{num_stations}")
|
| 163 |
+
async def get_route_info(num_stations: int = 25):
|
| 164 |
+
"""
|
| 165 |
+
Get metro route information
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
num_stations: Number of stations to include (default: 25)
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
Route information with all stations
|
| 172 |
+
"""
|
| 173 |
+
try:
|
| 174 |
+
generator = MetroDataGenerator(num_stations=num_stations)
|
| 175 |
+
route = generator.generate_route()
|
| 176 |
+
|
| 177 |
+
return {
|
| 178 |
+
"route": route.model_dump(),
|
| 179 |
+
"one_way_time_minutes": int((route.total_distance_km / route.avg_speed_kmh) * 60),
|
| 180 |
+
"round_trip_time_minutes": int((route.total_distance_km / route.avg_speed_kmh) * 60 * 2) + route.turnaround_time_minutes * 2
|
| 181 |
+
}
|
| 182 |
+
except Exception as e:
|
| 183 |
+
logger.error(f"Error generating route: {e}")
|
| 184 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@app.get("/api/v1/trains/health/{num_trains}")
|
| 188 |
+
async def get_train_health(num_trains: int = 25):
|
| 189 |
+
"""
|
| 190 |
+
Generate train health status data
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
num_trains: Number of trains (default: 25)
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
List of train health statuses
|
| 197 |
+
"""
|
| 198 |
+
try:
|
| 199 |
+
generator = MetroDataGenerator(num_trains=num_trains)
|
| 200 |
+
health_data = generator.generate_train_health_statuses()
|
| 201 |
+
|
| 202 |
+
summary = {
|
| 203 |
+
"total": len(health_data),
|
| 204 |
+
"fully_healthy": sum(1 for h in health_data if h.is_fully_healthy),
|
| 205 |
+
"partial": sum(1 for h in health_data if not h.is_fully_healthy and h.available_hours),
|
| 206 |
+
"unavailable": sum(1 for h in health_data if not h.is_fully_healthy and not h.available_hours)
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
return {
|
| 210 |
+
"summary": summary,
|
| 211 |
+
"trains": [h.dict() for h in health_data]
|
| 212 |
+
}
|
| 213 |
+
except Exception as e:
|
| 214 |
+
logger.error(f"Error generating train health: {e}")
|
| 215 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@app.get("/api/v1/depot/layout")
|
| 219 |
+
async def get_depot_layout():
|
| 220 |
+
"""Get depot bay layout information"""
|
| 221 |
+
try:
|
| 222 |
+
generator = MetroDataGenerator()
|
| 223 |
+
layout = generator.generate_depot_layout()
|
| 224 |
+
|
| 225 |
+
return {
|
| 226 |
+
"depot": "Muttom_Depot",
|
| 227 |
+
"layout": layout,
|
| 228 |
+
"total_bays": sum(len(bays) for bays in layout.values())
|
| 229 |
+
}
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logger.error(f"Error generating depot layout: {e}")
|
| 232 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@app.get("/api/v1/schedule/example")
|
| 236 |
+
async def get_example_schedule():
|
| 237 |
+
"""Get an example schedule for demonstration"""
|
| 238 |
+
request = ScheduleRequest(
|
| 239 |
+
date=datetime.now().strftime("%Y-%m-%d"),
|
| 240 |
+
num_trains=30,
|
| 241 |
+
num_stations=25,
|
| 242 |
+
min_service_trains=22,
|
| 243 |
+
min_standby_trains=4
|
| 244 |
+
)
|
| 245 |
+
return await generate_schedule(request)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# Error handlers
|
| 249 |
+
@app.exception_handler(404)
|
| 250 |
+
async def not_found_handler(request, exc):
|
| 251 |
+
return JSONResponse(
|
| 252 |
+
status_code=404,
|
| 253 |
+
content={
|
| 254 |
+
"error": "Not Found",
|
| 255 |
+
"message": "The requested resource was not found",
|
| 256 |
+
"path": str(request.url)
|
| 257 |
+
}
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@app.exception_handler(500)
|
| 262 |
+
async def internal_error_handler(request, exc):
|
| 263 |
+
logger.error(f"Internal server error: {exc}", exc_info=True)
|
| 264 |
+
return JSONResponse(
|
| 265 |
+
status_code=500,
|
| 266 |
+
content={
|
| 267 |
+
"error": "Internal Server Error",
|
| 268 |
+
"message": "An unexpected error occurred"
|
| 269 |
+
}
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
import uvicorn
|
| 275 |
+
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
DataService/metro_data_generator.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Metro Synthetic Data Generator
|
| 3 |
+
Generates realistic metro train scheduling data with time-based constraints
|
| 4 |
+
"""
|
| 5 |
+
import random
|
| 6 |
+
import uuid
|
| 7 |
+
from datetime import datetime, timedelta, time
|
| 8 |
+
from typing import List, Dict, Tuple
|
| 9 |
+
from .metro_models import (
|
| 10 |
+
TrainHealthStatus, Station, Route, FitnessCertificates,
|
| 11 |
+
FitnessCertificate, CertificateStatus, JobCards, Branding
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class MetroDataGenerator:
|
| 16 |
+
"""Generate synthetic data for metro train scheduling"""
|
| 17 |
+
|
| 18 |
+
STATIONS_ALUVA_PETTAH = [
|
| 19 |
+
"Aluva", "Pulinchodu", "Companypadi", "Ambattukavu", "Muttom",
|
| 20 |
+
"Kalamassery", "Cochin University", "Pathadipalam", "Edapally",
|
| 21 |
+
"Changampuzha Park", "Palarivattom", "J.L.N Stadium", "Kaloor",
|
| 22 |
+
"Town Hall", "M.G. Road", "Maharaja's College", "Ernakulam South",
|
| 23 |
+
"Kadavanthra", "Elamkulam", "Vyttila", "Thaikoodam", "Petta",
|
| 24 |
+
"Vadakkekotta", "SN Junction", "Pettah"
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
DEPOT_BAYS = [f"BAY-{str(i).zfill(2)}" for i in range(1, 16)]
|
| 28 |
+
IBL_BAYS = [f"IBL-{str(i).zfill(2)}" for i in range(1, 6)]
|
| 29 |
+
WASH_BAYS = [f"WASH-BAY-{str(i).zfill(2)}" for i in range(1, 4)]
|
| 30 |
+
|
| 31 |
+
ADVERTISERS = [
|
| 32 |
+
"COCACOLA-2024", "FLIPKART-FESTIVE", "AMAZON-PRIME",
|
| 33 |
+
"RELIANCE-JIO", "TATA-MOTORS", "SAMSUNG-GALAXY",
|
| 34 |
+
"NONE"
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
UNAVAILABLE_REASONS = [
|
| 38 |
+
"SCHEDULED_MAINTENANCE", "BRAKE_SYSTEM_REPAIR",
|
| 39 |
+
"HVAC_REPLACEMENT", "BOGIE_OVERHAUL", "ELECTRICAL_FAULT",
|
| 40 |
+
"ACCIDENT_DAMAGE", "PANTOGRAPH_REPAIR", "DOOR_SYSTEM_FAULT"
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
def __init__(self, num_trains: int = 25, num_stations: int = 25):
|
| 44 |
+
self.num_trains = num_trains
|
| 45 |
+
self.num_stations = min(num_stations, len(self.STATIONS_ALUVA_PETTAH))
|
| 46 |
+
self.trainset_ids = [f"TS-{str(i+1).zfill(3)}" for i in range(num_trains)]
|
| 47 |
+
|
| 48 |
+
def generate_route(self, route_name: str = "Aluva-Pettah Line") -> Route:
|
| 49 |
+
"""Generate metro route with stations"""
|
| 50 |
+
stations = []
|
| 51 |
+
total_distance = 25.612 # Actual KMRL distance
|
| 52 |
+
|
| 53 |
+
for i in range(self.num_stations):
|
| 54 |
+
distance = (total_distance / (self.num_stations - 1)) * i
|
| 55 |
+
station = Station(
|
| 56 |
+
station_id=f"STN-{str(i+1).zfill(3)}",
|
| 57 |
+
name=self.STATIONS_ALUVA_PETTAH[i],
|
| 58 |
+
sequence=i + 1,
|
| 59 |
+
distance_from_origin_km=round(distance, 2),
|
| 60 |
+
avg_dwell_time_seconds=random.randint(20, 45)
|
| 61 |
+
)
|
| 62 |
+
stations.append(station)
|
| 63 |
+
|
| 64 |
+
return Route(
|
| 65 |
+
route_id="KMRL-LINE-01",
|
| 66 |
+
name=route_name,
|
| 67 |
+
stations=stations,
|
| 68 |
+
total_distance_km=total_distance,
|
| 69 |
+
avg_speed_kmh=random.randint(32, 38),
|
| 70 |
+
turnaround_time_minutes=random.randint(8, 12)
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def generate_train_health_statuses(self) -> List[TrainHealthStatus]:
|
| 74 |
+
"""Generate health status for all trains"""
|
| 75 |
+
statuses = []
|
| 76 |
+
|
| 77 |
+
for i, ts_id in enumerate(self.trainset_ids):
|
| 78 |
+
# Determine train health category
|
| 79 |
+
health_roll = random.random()
|
| 80 |
+
|
| 81 |
+
if health_roll < 0.65: # 65% fully healthy
|
| 82 |
+
is_healthy = True
|
| 83 |
+
available_hours = None
|
| 84 |
+
reason = None
|
| 85 |
+
elif health_roll < 0.85: # 20% partially healthy
|
| 86 |
+
is_healthy = False
|
| 87 |
+
# Random availability window
|
| 88 |
+
start_hour = random.randint(5, 12)
|
| 89 |
+
end_hour = random.randint(start_hour + 4, 23)
|
| 90 |
+
available_hours = [(time(start_hour, 0), time(end_hour, 0))]
|
| 91 |
+
reason = f"Limited availability: {random.choice(['Minor repairs', 'Partial maintenance', 'Certificate renewal pending'])}"
|
| 92 |
+
else: # 15% unavailable
|
| 93 |
+
is_healthy = False
|
| 94 |
+
available_hours = []
|
| 95 |
+
reason = random.choice(self.UNAVAILABLE_REASONS)
|
| 96 |
+
|
| 97 |
+
status = TrainHealthStatus(
|
| 98 |
+
trainset_id=ts_id,
|
| 99 |
+
is_fully_healthy=is_healthy,
|
| 100 |
+
available_hours=available_hours,
|
| 101 |
+
unavailable_reason=reason,
|
| 102 |
+
cumulative_mileage=random.randint(50000, 200000),
|
| 103 |
+
days_since_maintenance=random.randint(1, 45),
|
| 104 |
+
component_health={
|
| 105 |
+
"brakes": random.uniform(0.7, 1.0),
|
| 106 |
+
"hvac": random.uniform(0.65, 1.0),
|
| 107 |
+
"doors": random.uniform(0.7, 1.0),
|
| 108 |
+
"bogies": random.uniform(0.75, 1.0),
|
| 109 |
+
"pantograph": random.uniform(0.7, 1.0),
|
| 110 |
+
"battery": random.uniform(0.65, 1.0),
|
| 111 |
+
"motor": random.uniform(0.75, 1.0)
|
| 112 |
+
}
|
| 113 |
+
)
|
| 114 |
+
statuses.append(status)
|
| 115 |
+
|
| 116 |
+
return statuses
|
| 117 |
+
|
| 118 |
+
def generate_fitness_certificates(self, train_id: str) -> FitnessCertificates:
|
| 119 |
+
"""Generate fitness certificates for a train"""
|
| 120 |
+
now = datetime.now()
|
| 121 |
+
|
| 122 |
+
def random_cert_status() -> Tuple[str, CertificateStatus]:
|
| 123 |
+
roll = random.random()
|
| 124 |
+
if roll < 0.75: # 75% valid
|
| 125 |
+
days_valid = random.randint(10, 60)
|
| 126 |
+
return (now + timedelta(days=days_valid)).isoformat(), CertificateStatus.VALID
|
| 127 |
+
elif roll < 0.90: # 15% expiring soon
|
| 128 |
+
days_valid = random.randint(1, 9)
|
| 129 |
+
return (now + timedelta(days=days_valid)).isoformat(), CertificateStatus.EXPIRING_SOON
|
| 130 |
+
else: # 10% expired
|
| 131 |
+
days_expired = random.randint(1, 5)
|
| 132 |
+
return (now - timedelta(days=days_expired)).isoformat(), CertificateStatus.EXPIRED
|
| 133 |
+
|
| 134 |
+
rs_date, rs_status = random_cert_status()
|
| 135 |
+
sig_date, sig_status = random_cert_status()
|
| 136 |
+
tel_date, tel_status = random_cert_status()
|
| 137 |
+
|
| 138 |
+
return FitnessCertificates(
|
| 139 |
+
rolling_stock=FitnessCertificate(valid_until=rs_date, status=rs_status),
|
| 140 |
+
signalling=FitnessCertificate(valid_until=sig_date, status=sig_status),
|
| 141 |
+
telecom=FitnessCertificate(valid_until=tel_date, status=tel_status)
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
def generate_job_cards(self, train_id: str) -> JobCards:
|
| 145 |
+
"""Generate job cards for a train"""
|
| 146 |
+
num_open = random.choices([0, 1, 2, 3, 4, 5], weights=[50, 25, 15, 7, 2, 1])[0]
|
| 147 |
+
|
| 148 |
+
blocking = []
|
| 149 |
+
if num_open > 0:
|
| 150 |
+
num_blocking = random.choices([0, 1, 2, 3], weights=[70, 20, 8, 2])[0]
|
| 151 |
+
if num_blocking > 0:
|
| 152 |
+
components = ["BRAKE", "HVAC", "DOOR", "BOGIE", "PANTOGRAPH", "ELECTRICAL"]
|
| 153 |
+
selected = random.sample(components, min(num_blocking, len(components)))
|
| 154 |
+
blocking = [f"JC-{random.randint(40000, 49999)}-{comp}" for comp in selected]
|
| 155 |
+
|
| 156 |
+
return JobCards(open=num_open, blocking=blocking)
|
| 157 |
+
|
| 158 |
+
def generate_branding(self) -> Branding:
|
| 159 |
+
"""Generate branding information"""
|
| 160 |
+
advertiser = random.choice(self.ADVERTISERS)
|
| 161 |
+
|
| 162 |
+
if advertiser == "NONE":
|
| 163 |
+
return Branding(
|
| 164 |
+
advertiser="NONE",
|
| 165 |
+
contract_hours_remaining=0,
|
| 166 |
+
exposure_priority="NONE"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
return Branding(
|
| 170 |
+
advertiser=advertiser,
|
| 171 |
+
contract_hours_remaining=random.randint(50, 500),
|
| 172 |
+
exposure_priority=random.choice(["LOW", "MEDIUM", "HIGH", "CRITICAL"])
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
def calculate_readiness_score(
|
| 176 |
+
self,
|
| 177 |
+
fitness_certs: FitnessCertificates,
|
| 178 |
+
job_cards: JobCards,
|
| 179 |
+
component_health: Dict[str, float]
|
| 180 |
+
) -> float:
|
| 181 |
+
"""Calculate overall readiness score for a train"""
|
| 182 |
+
score = 1.0
|
| 183 |
+
|
| 184 |
+
# Certificate penalties
|
| 185 |
+
if fitness_certs.rolling_stock.status == CertificateStatus.EXPIRED:
|
| 186 |
+
score -= 0.4
|
| 187 |
+
elif fitness_certs.rolling_stock.status == CertificateStatus.EXPIRING_SOON:
|
| 188 |
+
score -= 0.1
|
| 189 |
+
|
| 190 |
+
if fitness_certs.signalling.status == CertificateStatus.EXPIRED:
|
| 191 |
+
score -= 0.3
|
| 192 |
+
elif fitness_certs.signalling.status == CertificateStatus.EXPIRING_SOON:
|
| 193 |
+
score -= 0.05
|
| 194 |
+
|
| 195 |
+
if fitness_certs.telecom.status == CertificateStatus.EXPIRED:
|
| 196 |
+
score -= 0.2
|
| 197 |
+
elif fitness_certs.telecom.status == CertificateStatus.EXPIRING_SOON:
|
| 198 |
+
score -= 0.05
|
| 199 |
+
|
| 200 |
+
# Job card penalties
|
| 201 |
+
if job_cards.open > 0:
|
| 202 |
+
score -= min(0.15, job_cards.open * 0.03)
|
| 203 |
+
if len(job_cards.blocking) > 0:
|
| 204 |
+
score -= min(0.25, len(job_cards.blocking) * 0.1)
|
| 205 |
+
|
| 206 |
+
# Component health impact
|
| 207 |
+
avg_health = sum(component_health.values()) / len(component_health)
|
| 208 |
+
health_factor = (avg_health - 0.5) * 0.2 # -0.1 to +0.1
|
| 209 |
+
score += health_factor
|
| 210 |
+
|
| 211 |
+
return max(0.0, min(1.0, score))
|
| 212 |
+
|
| 213 |
+
def generate_depot_layout(self) -> Dict[str, List[str]]:
|
| 214 |
+
"""Generate depot bay layout"""
|
| 215 |
+
return {
|
| 216 |
+
"stabling_bays": self.DEPOT_BAYS.copy(),
|
| 217 |
+
"ibl_bays": self.IBL_BAYS.copy(),
|
| 218 |
+
"wash_bays": self.WASH_BAYS.copy()
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
def get_realistic_mileage_distribution(self, num_trains: int) -> List[int]:
|
| 222 |
+
"""Generate realistic cumulative mileage distribution"""
|
| 223 |
+
# Create a distribution with some variance
|
| 224 |
+
base_mileage = 120000
|
| 225 |
+
mileages = []
|
| 226 |
+
|
| 227 |
+
for i in range(num_trains):
|
| 228 |
+
# Add variance based on age and usage patterns
|
| 229 |
+
variance = random.randint(-40000, 50000)
|
| 230 |
+
mileage = base_mileage + variance
|
| 231 |
+
mileages.append(max(50000, min(200000, mileage)))
|
| 232 |
+
|
| 233 |
+
return mileages
|
DataService/metro_models.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data models for Metro Train Scheduling System
|
| 3 |
+
Comprehensive models matching the KMRL (Kochi Metro Rail Limited) structure
|
| 4 |
+
"""
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from typing import List, Optional, Dict, Literal
|
| 7 |
+
from datetime import datetime, time
|
| 8 |
+
from enum import Enum
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TrainStatus(str, Enum):
|
| 12 |
+
"""Train operational status"""
|
| 13 |
+
REVENUE_SERVICE = "REVENUE_SERVICE"
|
| 14 |
+
STANDBY = "STANDBY"
|
| 15 |
+
MAINTENANCE = "MAINTENANCE"
|
| 16 |
+
CLEANING = "CLEANING"
|
| 17 |
+
OUT_OF_SERVICE = "OUT_OF_SERVICE"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class CertificateStatus(str, Enum):
|
| 21 |
+
"""Fitness certificate status"""
|
| 22 |
+
VALID = "VALID"
|
| 23 |
+
EXPIRING_SOON = "EXPIRING_SOON"
|
| 24 |
+
EXPIRED = "EXPIRED"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class MaintenanceType(str, Enum):
|
| 28 |
+
"""Types of maintenance operations"""
|
| 29 |
+
SCHEDULED_INSPECTION = "SCHEDULED_INSPECTION"
|
| 30 |
+
PREVENTIVE = "PREVENTIVE"
|
| 31 |
+
CORRECTIVE = "CORRECTIVE"
|
| 32 |
+
BREAKDOWN = "BREAKDOWN"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Severity(str, Enum):
|
| 36 |
+
"""Alert severity levels"""
|
| 37 |
+
LOW = "LOW"
|
| 38 |
+
MEDIUM = "MEDIUM"
|
| 39 |
+
HIGH = "HIGH"
|
| 40 |
+
CRITICAL = "CRITICAL"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class FitnessCertificate(BaseModel):
|
| 44 |
+
"""Individual fitness certificate"""
|
| 45 |
+
valid_until: str # ISO format date
|
| 46 |
+
status: CertificateStatus
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class FitnessCertificates(BaseModel):
|
| 50 |
+
"""All fitness certificates for a trainset"""
|
| 51 |
+
rolling_stock: FitnessCertificate
|
| 52 |
+
signalling: FitnessCertificate
|
| 53 |
+
telecom: FitnessCertificate
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class JobCards(BaseModel):
|
| 57 |
+
"""Job cards and maintenance tasks"""
|
| 58 |
+
open: int
|
| 59 |
+
blocking: List[str] = Field(default_factory=list)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class Branding(BaseModel):
|
| 63 |
+
"""Advertising/branding information"""
|
| 64 |
+
advertiser: str
|
| 65 |
+
contract_hours_remaining: int
|
| 66 |
+
exposure_priority: Literal["NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class ServiceBlock(BaseModel):
|
| 70 |
+
"""A service block represents a continuous operating period"""
|
| 71 |
+
block_id: str
|
| 72 |
+
departure_time: str # HH:MM format
|
| 73 |
+
origin: str
|
| 74 |
+
destination: str
|
| 75 |
+
trip_count: int # Number of round trips in this block
|
| 76 |
+
estimated_km: int
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class Trainset(BaseModel):
|
| 80 |
+
"""Complete trainset information"""
|
| 81 |
+
trainset_id: str
|
| 82 |
+
status: TrainStatus
|
| 83 |
+
priority_rank: Optional[int] = None
|
| 84 |
+
assigned_duty: Optional[str] = None
|
| 85 |
+
|
| 86 |
+
# Service blocks for revenue service trains
|
| 87 |
+
service_blocks: List[ServiceBlock] = Field(default_factory=list)
|
| 88 |
+
|
| 89 |
+
# Maintenance information
|
| 90 |
+
maintenance_type: Optional[MaintenanceType] = None
|
| 91 |
+
ibl_bay: Optional[str] = None # Inspection/Berthing Location
|
| 92 |
+
estimated_completion: Optional[str] = None
|
| 93 |
+
|
| 94 |
+
# Cleaning information
|
| 95 |
+
cleaning_bay: Optional[str] = None
|
| 96 |
+
cleaning_type: Optional[str] = None
|
| 97 |
+
scheduled_service_start: Optional[str] = None
|
| 98 |
+
|
| 99 |
+
# Operational metrics
|
| 100 |
+
daily_km_allocation: int
|
| 101 |
+
cumulative_km: int
|
| 102 |
+
stabling_bay: Optional[str] = None
|
| 103 |
+
|
| 104 |
+
# Compliance and health
|
| 105 |
+
fitness_certificates: FitnessCertificates
|
| 106 |
+
job_cards: JobCards
|
| 107 |
+
|
| 108 |
+
# Branding
|
| 109 |
+
branding: Optional[Branding] = None
|
| 110 |
+
|
| 111 |
+
# Computed scores
|
| 112 |
+
readiness_score: float = Field(ge=0.0, le=1.0)
|
| 113 |
+
constraints_met: bool
|
| 114 |
+
|
| 115 |
+
# Alerts
|
| 116 |
+
alerts: List[str] = Field(default_factory=list)
|
| 117 |
+
standby_reason: Optional[str] = None
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class FleetSummary(BaseModel):
|
| 121 |
+
"""Summary statistics for the entire fleet"""
|
| 122 |
+
total_trainsets: int
|
| 123 |
+
revenue_service: int
|
| 124 |
+
standby: int
|
| 125 |
+
maintenance: int
|
| 126 |
+
cleaning: int
|
| 127 |
+
availability_percent: float
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class OptimizationMetrics(BaseModel):
|
| 131 |
+
"""Metrics about the optimization result"""
|
| 132 |
+
mileage_variance_coefficient: float
|
| 133 |
+
avg_readiness_score: float
|
| 134 |
+
branding_sla_compliance: float
|
| 135 |
+
shunting_movements_required: int
|
| 136 |
+
total_planned_km: int
|
| 137 |
+
fitness_expiry_violations: int
|
| 138 |
+
optimization_runtime_ms: int = 0
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class Alert(BaseModel):
|
| 142 |
+
"""Alert or conflict in the schedule"""
|
| 143 |
+
trainset_id: str
|
| 144 |
+
severity: Severity
|
| 145 |
+
type: str
|
| 146 |
+
message: str
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class DecisionRationale(BaseModel):
|
| 150 |
+
"""Explanation of optimization decisions"""
|
| 151 |
+
algorithm_version: str
|
| 152 |
+
objective_weights: Dict[str, float]
|
| 153 |
+
constraint_violations: int
|
| 154 |
+
optimization_runtime_ms: int
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class DaySchedule(BaseModel):
|
| 158 |
+
"""Complete daily schedule for all trains"""
|
| 159 |
+
schedule_id: str
|
| 160 |
+
generated_at: str # ISO format with timezone
|
| 161 |
+
valid_from: str # ISO format
|
| 162 |
+
valid_until: str # ISO format
|
| 163 |
+
depot: str
|
| 164 |
+
|
| 165 |
+
trainsets: List[Trainset]
|
| 166 |
+
fleet_summary: FleetSummary
|
| 167 |
+
optimization_metrics: OptimizationMetrics
|
| 168 |
+
conflicts_and_alerts: List[Alert]
|
| 169 |
+
decision_rationale: DecisionRationale
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class Station(BaseModel):
|
| 173 |
+
"""Metro station information"""
|
| 174 |
+
station_id: str
|
| 175 |
+
name: str
|
| 176 |
+
sequence: int # Position in the line (1-25)
|
| 177 |
+
distance_from_origin_km: float
|
| 178 |
+
avg_dwell_time_seconds: int = 30 # Average stopping time
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class Route(BaseModel):
|
| 182 |
+
"""Single metro line route"""
|
| 183 |
+
route_id: str
|
| 184 |
+
name: str
|
| 185 |
+
stations: List[Station]
|
| 186 |
+
total_distance_km: float
|
| 187 |
+
avg_speed_kmh: float = 35
|
| 188 |
+
turnaround_time_minutes: int = 10 # Time needed at terminals
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class OperationalHours(BaseModel):
|
| 192 |
+
"""Service hours configuration"""
|
| 193 |
+
start_time: time = time(5, 0) # 5:00 AM
|
| 194 |
+
end_time: time = time(23, 0) # 11:00 PM
|
| 195 |
+
peak_hours: List[tuple] = Field(
|
| 196 |
+
default_factory=lambda: [
|
| 197 |
+
(time(7, 0), time(10, 0)), # Morning peak
|
| 198 |
+
(time(17, 0), time(20, 0)) # Evening peak
|
| 199 |
+
]
|
| 200 |
+
)
|
| 201 |
+
peak_frequency_minutes: int = 5 # Train every 5 minutes during peak
|
| 202 |
+
off_peak_frequency_minutes: int = 10 # Train every 10 minutes off-peak
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
class TrainHealthStatus(BaseModel):
|
| 206 |
+
"""Health status for optimization"""
|
| 207 |
+
trainset_id: str
|
| 208 |
+
is_fully_healthy: bool
|
| 209 |
+
available_hours: Optional[List[tuple]] = None # (start_hour, end_hour) if partial
|
| 210 |
+
unavailable_reason: Optional[str] = None
|
| 211 |
+
cumulative_mileage: int
|
| 212 |
+
days_since_maintenance: int
|
| 213 |
+
component_health: Dict[str, float] # Component: health_score (0-1)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class ScheduleRequest(BaseModel):
|
| 217 |
+
"""Request for schedule generation"""
|
| 218 |
+
date: str # YYYY-MM-DD
|
| 219 |
+
num_trains: int = Field(default=25, ge=15, le=40)
|
| 220 |
+
num_stations: int = Field(default=25, ge=10, le=50)
|
| 221 |
+
route_name: str = "Aluva-Pettah Line"
|
| 222 |
+
depot_name: str = "Muttom_Depot"
|
| 223 |
+
|
| 224 |
+
# Optional: override train health
|
| 225 |
+
train_health_overrides: Optional[List[TrainHealthStatus]] = None
|
| 226 |
+
|
| 227 |
+
# Optimization parameters
|
| 228 |
+
min_service_trains: int = 20
|
| 229 |
+
min_standby_trains: int = 2
|
| 230 |
+
max_daily_km_per_train: int = 300
|
| 231 |
+
balance_mileage: bool = True
|
| 232 |
+
prioritize_branding: bool = True
|
DataService/schedule_optimizer.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Metro Train Schedule Optimizer
|
| 3 |
+
Generates optimal daily schedules from 5:00 AM to 11:00 PM
|
| 4 |
+
Considers train health, maintenance, branding, and mileage balancing
|
| 5 |
+
"""
|
| 6 |
+
import random
|
| 7 |
+
from datetime import datetime, time, timedelta
|
| 8 |
+
from typing import List, Dict, Tuple, Optional
|
| 9 |
+
from .metro_models import (
|
| 10 |
+
DaySchedule, Trainset, TrainStatus, ServiceBlock, FleetSummary,
|
| 11 |
+
OptimizationMetrics, Alert, Severity, DecisionRationale,
|
| 12 |
+
TrainHealthStatus, Route, OperationalHours, FitnessCertificates,
|
| 13 |
+
JobCards, Branding, CertificateStatus, MaintenanceType
|
| 14 |
+
)
|
| 15 |
+
from .metro_data_generator import MetroDataGenerator
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MetroScheduleOptimizer:
|
| 19 |
+
"""Optimize daily metro train schedules"""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
date: str,
|
| 24 |
+
num_trains: int,
|
| 25 |
+
route: Route,
|
| 26 |
+
train_health: List[TrainHealthStatus],
|
| 27 |
+
depot_name: str = "Muttom_Depot"
|
| 28 |
+
):
|
| 29 |
+
self.date = date
|
| 30 |
+
self.num_trains = num_trains
|
| 31 |
+
self.route = route
|
| 32 |
+
self.train_health = {t.trainset_id: t for t in train_health}
|
| 33 |
+
self.depot_name = depot_name
|
| 34 |
+
self.generator = MetroDataGenerator(num_trains)
|
| 35 |
+
|
| 36 |
+
# Operating parameters
|
| 37 |
+
self.op_hours = OperationalHours()
|
| 38 |
+
self.one_way_time_minutes = int(
|
| 39 |
+
(route.total_distance_km / route.avg_speed_kmh) * 60
|
| 40 |
+
)
|
| 41 |
+
self.round_trip_time_minutes = (
|
| 42 |
+
self.one_way_time_minutes * 2 + route.turnaround_time_minutes * 2
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Pre-generate train data
|
| 46 |
+
self.train_data = self._initialize_train_data()
|
| 47 |
+
|
| 48 |
+
def _initialize_train_data(self) -> Dict[str, Dict]:
|
| 49 |
+
"""Initialize all train-specific data"""
|
| 50 |
+
data = {}
|
| 51 |
+
mileages = self.generator.get_realistic_mileage_distribution(self.num_trains)
|
| 52 |
+
|
| 53 |
+
for i, train_id in enumerate(self.generator.trainset_ids):
|
| 54 |
+
health = self.train_health[train_id]
|
| 55 |
+
fitness_certs = self.generator.generate_fitness_certificates(train_id)
|
| 56 |
+
job_cards = self.generator.generate_job_cards(train_id)
|
| 57 |
+
branding = self.generator.generate_branding()
|
| 58 |
+
|
| 59 |
+
readiness = self.generator.calculate_readiness_score(
|
| 60 |
+
fitness_certs, job_cards, health.component_health
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
data[train_id] = {
|
| 64 |
+
"health": health,
|
| 65 |
+
"fitness_certs": fitness_certs,
|
| 66 |
+
"job_cards": job_cards,
|
| 67 |
+
"branding": branding,
|
| 68 |
+
"readiness_score": readiness,
|
| 69 |
+
"cumulative_km": mileages[i],
|
| 70 |
+
"stabling_bay": random.choice(self.generator.DEPOT_BAYS)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return data
|
| 74 |
+
|
| 75 |
+
def _calculate_service_hours(self) -> int:
|
| 76 |
+
"""Calculate total service hours in a day"""
|
| 77 |
+
start = datetime.combine(datetime.today(), self.op_hours.start_time)
|
| 78 |
+
end = datetime.combine(datetime.today(), self.op_hours.end_time)
|
| 79 |
+
return int((end - start).total_seconds() / 3600)
|
| 80 |
+
|
| 81 |
+
def _is_train_available(
|
| 82 |
+
self,
|
| 83 |
+
train_id: str,
|
| 84 |
+
start_hour: int,
|
| 85 |
+
end_hour: int
|
| 86 |
+
) -> bool:
|
| 87 |
+
"""Check if train is available for given time window"""
|
| 88 |
+
health = self.train_data[train_id]["health"]
|
| 89 |
+
|
| 90 |
+
if health.is_fully_healthy:
|
| 91 |
+
return True
|
| 92 |
+
|
| 93 |
+
if not health.available_hours:
|
| 94 |
+
return False
|
| 95 |
+
|
| 96 |
+
# Check if requested window overlaps with available hours
|
| 97 |
+
for avail_start, avail_end in health.available_hours:
|
| 98 |
+
req_start = time(start_hour, 0)
|
| 99 |
+
req_end = time(end_hour, 0)
|
| 100 |
+
|
| 101 |
+
if req_start >= avail_start and req_end <= avail_end:
|
| 102 |
+
return True
|
| 103 |
+
|
| 104 |
+
return False
|
| 105 |
+
|
| 106 |
+
def _rank_trains_for_service(self) -> List[Tuple[str, float]]:
|
| 107 |
+
"""Rank trains by suitability for revenue service"""
|
| 108 |
+
rankings = []
|
| 109 |
+
|
| 110 |
+
for train_id, data in self.train_data.items():
|
| 111 |
+
score = 0.0
|
| 112 |
+
|
| 113 |
+
# Base readiness score (40% weight)
|
| 114 |
+
score += data["readiness_score"] * 0.4
|
| 115 |
+
|
| 116 |
+
# Certificate validity (20% weight)
|
| 117 |
+
certs = data["fitness_certs"]
|
| 118 |
+
if certs.rolling_stock.status == CertificateStatus.VALID:
|
| 119 |
+
score += 0.15
|
| 120 |
+
if certs.signalling.status == CertificateStatus.VALID:
|
| 121 |
+
score += 0.05
|
| 122 |
+
|
| 123 |
+
# No blocking job cards (15% weight)
|
| 124 |
+
if len(data["job_cards"].blocking) == 0:
|
| 125 |
+
score += 0.15
|
| 126 |
+
|
| 127 |
+
# Branding priority (15% weight)
|
| 128 |
+
branding = data["branding"]
|
| 129 |
+
if branding.exposure_priority == "CRITICAL":
|
| 130 |
+
score += 0.15
|
| 131 |
+
elif branding.exposure_priority == "HIGH":
|
| 132 |
+
score += 0.10
|
| 133 |
+
elif branding.exposure_priority == "MEDIUM":
|
| 134 |
+
score += 0.05
|
| 135 |
+
|
| 136 |
+
# Mileage balancing (10% weight) - prefer lower mileage
|
| 137 |
+
max_mileage = 200000
|
| 138 |
+
mileage_factor = 1.0 - (data["cumulative_km"] / max_mileage)
|
| 139 |
+
score += mileage_factor * 0.10
|
| 140 |
+
|
| 141 |
+
rankings.append((train_id, score))
|
| 142 |
+
|
| 143 |
+
return sorted(rankings, key=lambda x: x[1], reverse=True)
|
| 144 |
+
|
| 145 |
+
def _generate_service_blocks(
|
| 146 |
+
self,
|
| 147 |
+
train_id: str,
|
| 148 |
+
duty_name: str,
|
| 149 |
+
num_blocks: int = 2
|
| 150 |
+
) -> Tuple[List[ServiceBlock], int]:
|
| 151 |
+
"""Generate service blocks for a train"""
|
| 152 |
+
blocks = []
|
| 153 |
+
total_km = 0
|
| 154 |
+
|
| 155 |
+
# Distribute service across the day
|
| 156 |
+
service_hours = self._calculate_service_hours()
|
| 157 |
+
block_duration_hours = service_hours // num_blocks
|
| 158 |
+
|
| 159 |
+
current_hour = self.op_hours.start_time.hour
|
| 160 |
+
|
| 161 |
+
for i in range(num_blocks):
|
| 162 |
+
block_start_hour = current_hour + (i * block_duration_hours)
|
| 163 |
+
if block_start_hour >= self.op_hours.end_time.hour:
|
| 164 |
+
break
|
| 165 |
+
|
| 166 |
+
# Calculate trips for this block
|
| 167 |
+
block_minutes = block_duration_hours * 60
|
| 168 |
+
trips = max(1, block_minutes // self.round_trip_time_minutes)
|
| 169 |
+
|
| 170 |
+
# Alternate origin/destination
|
| 171 |
+
if i % 2 == 0:
|
| 172 |
+
origin = self.route.stations[0].name
|
| 173 |
+
destination = self.route.stations[-1].name
|
| 174 |
+
else:
|
| 175 |
+
origin = self.route.stations[-1].name
|
| 176 |
+
destination = self.route.stations[0].name
|
| 177 |
+
|
| 178 |
+
block_km = int(trips * self.route.total_distance_km * 2) # Round trips
|
| 179 |
+
total_km += block_km
|
| 180 |
+
|
| 181 |
+
block = ServiceBlock(
|
| 182 |
+
block_id=f"BLK-{random.randint(1, 999):03d}",
|
| 183 |
+
departure_time=f"{block_start_hour:02d}:{random.randint(0, 45):02d}",
|
| 184 |
+
origin=origin,
|
| 185 |
+
destination=destination,
|
| 186 |
+
trip_count=trips,
|
| 187 |
+
estimated_km=block_km
|
| 188 |
+
)
|
| 189 |
+
blocks.append(block)
|
| 190 |
+
|
| 191 |
+
return blocks, total_km
|
| 192 |
+
|
| 193 |
+
def _assign_train_status(
|
| 194 |
+
self,
|
| 195 |
+
train_id: str,
|
| 196 |
+
rank: int,
|
| 197 |
+
required_service: int,
|
| 198 |
+
min_standby: int
|
| 199 |
+
) -> Tuple[TrainStatus, Optional[str], List[ServiceBlock], int]:
|
| 200 |
+
"""Assign status and duty to a train"""
|
| 201 |
+
data = self.train_data[train_id]
|
| 202 |
+
health = data["health"]
|
| 203 |
+
|
| 204 |
+
# Check if train is unavailable
|
| 205 |
+
if not health.is_fully_healthy and not health.available_hours:
|
| 206 |
+
# Determine maintenance or out of service
|
| 207 |
+
if data["job_cards"].open > 0 or len(data["job_cards"].blocking) > 0:
|
| 208 |
+
return TrainStatus.MAINTENANCE, None, [], 0
|
| 209 |
+
else:
|
| 210 |
+
return TrainStatus.MAINTENANCE, None, [], 0
|
| 211 |
+
|
| 212 |
+
# Check for blocking maintenance
|
| 213 |
+
if len(data["job_cards"].blocking) > 0:
|
| 214 |
+
return TrainStatus.MAINTENANCE, None, [], 0
|
| 215 |
+
|
| 216 |
+
# Check for expired certificates
|
| 217 |
+
certs = data["fitness_certs"]
|
| 218 |
+
if certs.rolling_stock.status == CertificateStatus.EXPIRED:
|
| 219 |
+
return TrainStatus.MAINTENANCE, None, [], 0
|
| 220 |
+
|
| 221 |
+
# Assign to revenue service
|
| 222 |
+
if rank <= required_service:
|
| 223 |
+
# Check availability for full day
|
| 224 |
+
if self._is_train_available(
|
| 225 |
+
train_id,
|
| 226 |
+
self.op_hours.start_time.hour,
|
| 227 |
+
self.op_hours.end_time.hour
|
| 228 |
+
):
|
| 229 |
+
duty = f"DUTY-{chr(65 + (rank-1) // 10)}{(rank-1) % 10 + 1}"
|
| 230 |
+
blocks, km = self._generate_service_blocks(train_id, duty)
|
| 231 |
+
return TrainStatus.REVENUE_SERVICE, duty, blocks, km
|
| 232 |
+
|
| 233 |
+
# Assign to standby
|
| 234 |
+
if rank <= required_service + min_standby:
|
| 235 |
+
return TrainStatus.STANDBY, None, [], 0
|
| 236 |
+
|
| 237 |
+
# Random assignment of remaining trains
|
| 238 |
+
roll = random.random()
|
| 239 |
+
if roll < 0.05:
|
| 240 |
+
return TrainStatus.CLEANING, None, [], 0
|
| 241 |
+
elif roll < 0.15:
|
| 242 |
+
return TrainStatus.STANDBY, None, [], 0
|
| 243 |
+
else:
|
| 244 |
+
return TrainStatus.MAINTENANCE, None, [], 0
|
| 245 |
+
|
| 246 |
+
def optimize_schedule(
|
| 247 |
+
self,
|
| 248 |
+
min_service_trains: int = 20,
|
| 249 |
+
min_standby: int = 2,
|
| 250 |
+
max_daily_km: int = 300
|
| 251 |
+
) -> DaySchedule:
|
| 252 |
+
"""Generate optimized daily schedule"""
|
| 253 |
+
start_time = datetime.now()
|
| 254 |
+
|
| 255 |
+
# Rank trains
|
| 256 |
+
rankings = self._rank_trains_for_service()
|
| 257 |
+
|
| 258 |
+
# Build trainset list
|
| 259 |
+
trainsets = []
|
| 260 |
+
status_counts = {
|
| 261 |
+
TrainStatus.REVENUE_SERVICE: 0,
|
| 262 |
+
TrainStatus.STANDBY: 0,
|
| 263 |
+
TrainStatus.MAINTENANCE: 0,
|
| 264 |
+
TrainStatus.CLEANING: 0
|
| 265 |
+
}
|
| 266 |
+
total_km = 0
|
| 267 |
+
readiness_scores = []
|
| 268 |
+
|
| 269 |
+
for rank, (train_id, score) in enumerate(rankings, 1):
|
| 270 |
+
data = self.train_data[train_id]
|
| 271 |
+
|
| 272 |
+
# Assign status and blocks
|
| 273 |
+
status, duty, blocks, daily_km = self._assign_train_status(
|
| 274 |
+
train_id, rank, min_service_trains, min_standby
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
status_counts[status] += 1
|
| 278 |
+
total_km += daily_km
|
| 279 |
+
readiness_scores.append(data["readiness_score"])
|
| 280 |
+
|
| 281 |
+
# Build trainset object
|
| 282 |
+
trainset = Trainset(
|
| 283 |
+
trainset_id=train_id,
|
| 284 |
+
status=status,
|
| 285 |
+
priority_rank=rank if status == TrainStatus.REVENUE_SERVICE else None,
|
| 286 |
+
assigned_duty=duty,
|
| 287 |
+
service_blocks=blocks,
|
| 288 |
+
daily_km_allocation=daily_km,
|
| 289 |
+
cumulative_km=data["cumulative_km"],
|
| 290 |
+
stabling_bay=data["stabling_bay"] if status != TrainStatus.MAINTENANCE else None,
|
| 291 |
+
fitness_certificates=data["fitness_certs"],
|
| 292 |
+
job_cards=data["job_cards"],
|
| 293 |
+
branding=data["branding"],
|
| 294 |
+
readiness_score=data["readiness_score"],
|
| 295 |
+
constraints_met=data["readiness_score"] >= 0.7
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# Add status-specific fields
|
| 299 |
+
if status == TrainStatus.MAINTENANCE:
|
| 300 |
+
trainset.maintenance_type = MaintenanceType.SCHEDULED_INSPECTION
|
| 301 |
+
trainset.ibl_bay = random.choice(self.generator.IBL_BAYS)
|
| 302 |
+
completion_time = datetime.now() + timedelta(hours=random.randint(4, 12))
|
| 303 |
+
trainset.estimated_completion = completion_time.isoformat()
|
| 304 |
+
elif status == TrainStatus.CLEANING:
|
| 305 |
+
trainset.cleaning_bay = random.choice(self.generator.WASH_BAYS)
|
| 306 |
+
trainset.cleaning_type = random.choice(["DEEP_INTERIOR", "EXTERIOR", "FULL"])
|
| 307 |
+
completion_time = datetime.now() + timedelta(hours=random.randint(2, 4))
|
| 308 |
+
trainset.estimated_completion = completion_time.isoformat()
|
| 309 |
+
trainset.scheduled_service_start = f"{random.randint(12, 18):02d}:30"
|
| 310 |
+
elif status == TrainStatus.STANDBY:
|
| 311 |
+
trainset.standby_reason = random.choice([
|
| 312 |
+
"MILEAGE_BALANCING", "EMERGENCY_BACKUP", "PEAK_HOUR_RESERVE"
|
| 313 |
+
])
|
| 314 |
+
|
| 315 |
+
# Generate alerts
|
| 316 |
+
alerts = []
|
| 317 |
+
if data["fitness_certs"].telecom.status == CertificateStatus.EXPIRING_SOON:
|
| 318 |
+
alerts.append("TELECOM_CERT_EXPIRES_SOON")
|
| 319 |
+
if len(data["job_cards"].blocking) > 0:
|
| 320 |
+
alerts.append(f"{len(data['job_cards'].blocking)}_BLOCKING_JOB_CARDS")
|
| 321 |
+
trainset.alerts = alerts
|
| 322 |
+
|
| 323 |
+
trainsets.append(trainset)
|
| 324 |
+
|
| 325 |
+
# Build fleet summary
|
| 326 |
+
fleet_summary = FleetSummary(
|
| 327 |
+
total_trainsets=self.num_trains,
|
| 328 |
+
revenue_service=status_counts[TrainStatus.REVENUE_SERVICE],
|
| 329 |
+
standby=status_counts[TrainStatus.STANDBY],
|
| 330 |
+
maintenance=status_counts[TrainStatus.MAINTENANCE],
|
| 331 |
+
cleaning=status_counts[TrainStatus.CLEANING],
|
| 332 |
+
availability_percent=round(
|
| 333 |
+
(status_counts[TrainStatus.REVENUE_SERVICE] + status_counts[TrainStatus.STANDBY])
|
| 334 |
+
/ self.num_trains * 100, 1
|
| 335 |
+
)
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Calculate optimization metrics
|
| 339 |
+
mileages = [data["cumulative_km"] for data in self.train_data.values()]
|
| 340 |
+
variance = (max(mileages) - min(mileages)) / (sum(mileages) / len(mileages))
|
| 341 |
+
|
| 342 |
+
optimization_metrics = OptimizationMetrics(
|
| 343 |
+
mileage_variance_coefficient=round(variance, 3),
|
| 344 |
+
avg_readiness_score=round(sum(readiness_scores) / len(readiness_scores), 2),
|
| 345 |
+
branding_sla_compliance=1.0, # Placeholder
|
| 346 |
+
shunting_movements_required=random.randint(5, 15),
|
| 347 |
+
total_planned_km=total_km,
|
| 348 |
+
fitness_expiry_violations=0
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Generate alerts
|
| 352 |
+
conflicts = []
|
| 353 |
+
for trainset in trainsets:
|
| 354 |
+
data = self.train_data[trainset.trainset_id]
|
| 355 |
+
|
| 356 |
+
if data["fitness_certs"].telecom.status == CertificateStatus.EXPIRING_SOON:
|
| 357 |
+
conflicts.append(Alert(
|
| 358 |
+
trainset_id=trainset.trainset_id,
|
| 359 |
+
severity=Severity.MEDIUM,
|
| 360 |
+
type="CERTIFICATE_EXPIRING",
|
| 361 |
+
message="Telecom certificate expires soon"
|
| 362 |
+
))
|
| 363 |
+
|
| 364 |
+
if len(data["job_cards"].blocking) > 0:
|
| 365 |
+
conflicts.append(Alert(
|
| 366 |
+
trainset_id=trainset.trainset_id,
|
| 367 |
+
severity=Severity.HIGH,
|
| 368 |
+
type="BLOCKING_MAINTENANCE",
|
| 369 |
+
message=f"{len(data['job_cards'].blocking)} open job cards preventing service"
|
| 370 |
+
))
|
| 371 |
+
|
| 372 |
+
# Decision rationale
|
| 373 |
+
end_time = datetime.now()
|
| 374 |
+
runtime_ms = int((end_time - start_time).total_seconds() * 1000)
|
| 375 |
+
|
| 376 |
+
rationale = DecisionRationale(
|
| 377 |
+
algorithm_version="v2.5.0",
|
| 378 |
+
objective_weights={
|
| 379 |
+
"service_readiness": 0.35,
|
| 380 |
+
"mileage_balancing": 0.25,
|
| 381 |
+
"branding_priority": 0.20,
|
| 382 |
+
"operational_cost": 0.20
|
| 383 |
+
},
|
| 384 |
+
constraint_violations=0,
|
| 385 |
+
optimization_runtime_ms=runtime_ms
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
# Build complete schedule
|
| 389 |
+
schedule_id = f"KMRL-{self.date}-{random.choice(['DAWN', 'ALPHA', 'PRIME'])}"
|
| 390 |
+
now = datetime.now()
|
| 391 |
+
|
| 392 |
+
schedule = DaySchedule(
|
| 393 |
+
schedule_id=schedule_id,
|
| 394 |
+
generated_at=now.isoformat(),
|
| 395 |
+
valid_from=f"{self.date}T{self.op_hours.start_time.isoformat()}+05:30",
|
| 396 |
+
valid_until=f"{self.date}T{self.op_hours.end_time.isoformat()}+05:30",
|
| 397 |
+
depot=self.depot_name,
|
| 398 |
+
trainsets=trainsets,
|
| 399 |
+
fleet_summary=fleet_summary,
|
| 400 |
+
optimization_metrics=optimization_metrics,
|
| 401 |
+
conflicts_and_alerts=conflicts,
|
| 402 |
+
decision_rationale=rationale
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
return schedule
|
demo_schedule.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Demo script to test Metro Train Scheduling System
|
| 3 |
+
Generates sample schedules and displays key information
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
# Add parent directory to path
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
|
| 13 |
+
from DataService.metro_data_generator import MetroDataGenerator
|
| 14 |
+
from DataService.schedule_optimizer import MetroScheduleOptimizer
|
| 15 |
+
from DataService.metro_models import ScheduleRequest
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def print_section(title: str):
|
| 19 |
+
"""Print a section header"""
|
| 20 |
+
print("\n" + "=" * 70)
|
| 21 |
+
print(f" {title}")
|
| 22 |
+
print("=" * 70)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def demo_data_generation():
|
| 26 |
+
"""Demonstrate data generation capabilities"""
|
| 27 |
+
print_section("DATA GENERATION DEMO")
|
| 28 |
+
|
| 29 |
+
# Initialize generator
|
| 30 |
+
generator = MetroDataGenerator(num_trains=30, num_stations=25)
|
| 31 |
+
print(f"\n✓ Initialized generator for {len(generator.trainset_ids)} trains")
|
| 32 |
+
|
| 33 |
+
# Generate route
|
| 34 |
+
route = generator.generate_route()
|
| 35 |
+
print(f"\n✓ Route: {route.name}")
|
| 36 |
+
print(f" - Total distance: {route.total_distance_km} km")
|
| 37 |
+
print(f" - Stations: {len(route.stations)}")
|
| 38 |
+
print(f" - First: {route.stations[0].name}")
|
| 39 |
+
print(f" - Last: {route.stations[-1].name}")
|
| 40 |
+
|
| 41 |
+
# Generate train health
|
| 42 |
+
health_statuses = generator.generate_train_health_statuses()
|
| 43 |
+
fully_healthy = sum(1 for h in health_statuses if h.is_fully_healthy)
|
| 44 |
+
partial = sum(1 for h in health_statuses if not h.is_fully_healthy and h.available_hours)
|
| 45 |
+
unavailable = sum(1 for h in health_statuses if not h.is_fully_healthy and not h.available_hours)
|
| 46 |
+
|
| 47 |
+
print(f"\n✓ Train Health Status:")
|
| 48 |
+
print(f" - Fully healthy: {fully_healthy} ({fully_healthy/len(health_statuses)*100:.1f}%)")
|
| 49 |
+
print(f" - Partially available: {partial} ({partial/len(health_statuses)*100:.1f}%)")
|
| 50 |
+
print(f" - Unavailable: {unavailable} ({unavailable/len(health_statuses)*100:.1f}%)")
|
| 51 |
+
|
| 52 |
+
# Show sample train details
|
| 53 |
+
sample_train = health_statuses[0]
|
| 54 |
+
print(f"\n✓ Sample Train: {sample_train.trainset_id}")
|
| 55 |
+
print(f" - Healthy: {sample_train.is_fully_healthy}")
|
| 56 |
+
print(f" - Cumulative mileage: {sample_train.cumulative_mileage:,} km")
|
| 57 |
+
print(f" - Days since maintenance: {sample_train.days_since_maintenance}")
|
| 58 |
+
print(f" - Component health: {len(sample_train.component_health)} components monitored")
|
| 59 |
+
|
| 60 |
+
return generator, route, health_statuses
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def demo_schedule_optimization(generator, route, health_statuses):
|
| 64 |
+
"""Demonstrate schedule optimization"""
|
| 65 |
+
print_section("SCHEDULE OPTIMIZATION DEMO")
|
| 66 |
+
|
| 67 |
+
date = datetime.now().strftime("%Y-%m-%d")
|
| 68 |
+
print(f"\n✓ Optimizing schedule for: {date}")
|
| 69 |
+
|
| 70 |
+
# Initialize optimizer
|
| 71 |
+
optimizer = MetroScheduleOptimizer(
|
| 72 |
+
date=date,
|
| 73 |
+
num_trains=30,
|
| 74 |
+
route=route,
|
| 75 |
+
train_health=health_statuses,
|
| 76 |
+
depot_name="Muttom_Depot"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
print(f" - Operating hours: 5:00 AM - 11:00 PM")
|
| 80 |
+
print(f" - One-way trip time: {optimizer.one_way_time_minutes} minutes")
|
| 81 |
+
print(f" - Round trip time: {optimizer.round_trip_time_minutes} minutes")
|
| 82 |
+
|
| 83 |
+
# Run optimization
|
| 84 |
+
print("\n✓ Running optimization...")
|
| 85 |
+
schedule = optimizer.optimize_schedule(
|
| 86 |
+
min_service_trains=22,
|
| 87 |
+
min_standby=3,
|
| 88 |
+
max_daily_km=300
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
print(f"\n✓ Schedule Generated: {schedule.schedule_id}")
|
| 92 |
+
print(f" - Generated at: {schedule.generated_at}")
|
| 93 |
+
print(f" - Valid period: {schedule.valid_from} to {schedule.valid_until}")
|
| 94 |
+
print(f" - Depot: {schedule.depot}")
|
| 95 |
+
|
| 96 |
+
return schedule
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def display_schedule_summary(schedule):
|
| 100 |
+
"""Display comprehensive schedule summary"""
|
| 101 |
+
print_section("SCHEDULE SUMMARY")
|
| 102 |
+
|
| 103 |
+
# Fleet summary
|
| 104 |
+
fs = schedule.fleet_summary
|
| 105 |
+
print(f"\n📊 Fleet Status:")
|
| 106 |
+
print(f" - Total trainsets: {fs.total_trainsets}")
|
| 107 |
+
print(f" - Revenue service: {fs.revenue_service}")
|
| 108 |
+
print(f" - Standby: {fs.standby}")
|
| 109 |
+
print(f" - Maintenance: {fs.maintenance}")
|
| 110 |
+
print(f" - Cleaning: {fs.cleaning}")
|
| 111 |
+
print(f" - Availability: {fs.availability_percent}%")
|
| 112 |
+
|
| 113 |
+
# Optimization metrics
|
| 114 |
+
om = schedule.optimization_metrics
|
| 115 |
+
print(f"\n📈 Optimization Metrics:")
|
| 116 |
+
print(f" - Total planned km: {om.total_planned_km:,} km")
|
| 117 |
+
print(f" - Avg readiness score: {om.avg_readiness_score:.2f}")
|
| 118 |
+
print(f" - Mileage variance: {om.mileage_variance_coefficient:.3f}")
|
| 119 |
+
print(f" - Branding SLA: {om.branding_sla_compliance:.1%}")
|
| 120 |
+
print(f" - Shunting movements: {om.shunting_movements_required}")
|
| 121 |
+
print(f" - Runtime: {om.optimization_runtime_ms} ms")
|
| 122 |
+
|
| 123 |
+
# Conflicts and alerts
|
| 124 |
+
if schedule.conflicts_and_alerts:
|
| 125 |
+
print(f"\n⚠️ Alerts and Conflicts: {len(schedule.conflicts_and_alerts)}")
|
| 126 |
+
for alert in schedule.conflicts_and_alerts[:5]: # Show first 5
|
| 127 |
+
print(f" - [{alert.severity}] {alert.trainset_id}: {alert.message}")
|
| 128 |
+
else:
|
| 129 |
+
print(f"\n✓ No conflicts or alerts")
|
| 130 |
+
|
| 131 |
+
# Decision rationale
|
| 132 |
+
dr = schedule.decision_rationale
|
| 133 |
+
print(f"\n🎯 Decision Rationale:")
|
| 134 |
+
print(f" - Algorithm version: {dr.algorithm_version}")
|
| 135 |
+
print(f" - Constraint violations: {dr.constraint_violations}")
|
| 136 |
+
print(f" - Objective weights:")
|
| 137 |
+
for obj, weight in dr.objective_weights.items():
|
| 138 |
+
print(f" • {obj}: {weight:.0%}")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def display_train_details(schedule):
|
| 142 |
+
"""Display details for selected trains"""
|
| 143 |
+
print_section("SAMPLE TRAIN DETAILS")
|
| 144 |
+
|
| 145 |
+
# Show one train from each status category
|
| 146 |
+
categories = {
|
| 147 |
+
"REVENUE_SERVICE": None,
|
| 148 |
+
"STANDBY": None,
|
| 149 |
+
"MAINTENANCE": None,
|
| 150 |
+
"CLEANING": None
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
for trainset in schedule.trainsets:
|
| 154 |
+
status = trainset.status.value
|
| 155 |
+
if status in categories and categories[status] is None:
|
| 156 |
+
categories[status] = trainset
|
| 157 |
+
|
| 158 |
+
for status, trainset in categories.items():
|
| 159 |
+
if trainset is None:
|
| 160 |
+
continue
|
| 161 |
+
|
| 162 |
+
print(f"\n🚇 {trainset.trainset_id} - {status}")
|
| 163 |
+
print(f" - Readiness score: {trainset.readiness_score:.2f}")
|
| 164 |
+
print(f" - Cumulative km: {trainset.cumulative_km:,} km")
|
| 165 |
+
print(f" - Daily km allocation: {trainset.daily_km_allocation} km")
|
| 166 |
+
|
| 167 |
+
if trainset.assigned_duty:
|
| 168 |
+
print(f" - Assigned duty: {trainset.assigned_duty}")
|
| 169 |
+
|
| 170 |
+
if trainset.service_blocks:
|
| 171 |
+
# Guard against non-iterable sentinel values (e.g., typing.Never) by
|
| 172 |
+
# ensuring we have a real sequence before slicing/iterating.
|
| 173 |
+
blocks = None
|
| 174 |
+
if isinstance(trainset.service_blocks, (list, tuple)):
|
| 175 |
+
blocks = trainset.service_blocks
|
| 176 |
+
else:
|
| 177 |
+
try:
|
| 178 |
+
blocks = list(trainset.service_blocks)
|
| 179 |
+
except Exception:
|
| 180 |
+
blocks = []
|
| 181 |
+
print(f" - Service blocks: {len(blocks)}")
|
| 182 |
+
for block in blocks[:2]: # Show first 2
|
| 183 |
+
print(f" • {block.block_id}: {block.origin} → {block.destination}")
|
| 184 |
+
print(f" Depart: {block.departure_time}, Trips: {block.trip_count}, Est: {block.estimated_km} km")
|
| 185 |
+
|
| 186 |
+
# Certificates
|
| 187 |
+
certs = trainset.fitness_certificates
|
| 188 |
+
print(f" - Certificates:")
|
| 189 |
+
print(f" • Rolling Stock: {certs.rolling_stock.status.value}")
|
| 190 |
+
print(f" • Signalling: {certs.signalling.status.value}")
|
| 191 |
+
print(f" • Telecom: {certs.telecom.status.value}")
|
| 192 |
+
|
| 193 |
+
# Job cards
|
| 194 |
+
if trainset.job_cards.open > 0:
|
| 195 |
+
print(f" - Job cards: {trainset.job_cards.open} open")
|
| 196 |
+
if trainset.job_cards.blocking:
|
| 197 |
+
print(f" • Blocking: {', '.join(trainset.job_cards.blocking)}")
|
| 198 |
+
|
| 199 |
+
# Branding
|
| 200 |
+
if trainset.branding and trainset.branding.advertiser != "NONE":
|
| 201 |
+
print(f" - Branding: {trainset.branding.advertiser}")
|
| 202 |
+
print(f" • Priority: {trainset.branding.exposure_priority}")
|
| 203 |
+
print(f" • Hours remaining: {trainset.branding.contract_hours_remaining}")
|
| 204 |
+
|
| 205 |
+
if trainset.alerts:
|
| 206 |
+
print(f" - Alerts: {', '.join(trainset.alerts)}")
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def save_schedule_json(schedule, filename="sample_schedule.json"):
|
| 210 |
+
"""Save schedule to JSON file"""
|
| 211 |
+
print_section("SAVING SCHEDULE")
|
| 212 |
+
|
| 213 |
+
schedule_dict = schedule.model_dump()
|
| 214 |
+
|
| 215 |
+
with open(filename, 'w') as f:
|
| 216 |
+
json.dump(schedule_dict, f, indent=2, default=str)
|
| 217 |
+
|
| 218 |
+
print(f"\n✓ Schedule saved to: {filename}")
|
| 219 |
+
print(f" - Size: {os.path.getsize(filename) / 1024:.1f} KB")
|
| 220 |
+
print(f" - Trainsets: {len(schedule_dict['trainsets'])}")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def main():
|
| 224 |
+
"""Main demo function"""
|
| 225 |
+
print("\n" + "🚇" * 35)
|
| 226 |
+
print(" METRO TRAIN SCHEDULING SYSTEM - DEMO")
|
| 227 |
+
print("🚇" * 35)
|
| 228 |
+
|
| 229 |
+
try:
|
| 230 |
+
# Step 1: Data generation
|
| 231 |
+
generator, route, health_statuses = demo_data_generation()
|
| 232 |
+
|
| 233 |
+
# Step 2: Schedule optimization
|
| 234 |
+
schedule = demo_schedule_optimization(generator, route, health_statuses)
|
| 235 |
+
|
| 236 |
+
# Step 3: Display results
|
| 237 |
+
display_schedule_summary(schedule)
|
| 238 |
+
display_train_details(schedule)
|
| 239 |
+
|
| 240 |
+
# Step 4: Save to file
|
| 241 |
+
save_schedule_json(schedule)
|
| 242 |
+
|
| 243 |
+
print_section("DEMO COMPLETE")
|
| 244 |
+
print("\n✓ All systems operational!")
|
| 245 |
+
print("\nNext steps:")
|
| 246 |
+
print(" 1. Review sample_schedule.json for full schedule details")
|
| 247 |
+
print(" 2. Run 'python run_api.py' to start the FastAPI service")
|
| 248 |
+
print(" 3. Visit http://localhost:8000/docs for API documentation")
|
| 249 |
+
print(" 4. Test with: curl http://localhost:8000/api/v1/schedule/example")
|
| 250 |
+
print()
|
| 251 |
+
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"\n❌ Error: {e}")
|
| 254 |
+
import traceback
|
| 255 |
+
traceback.print_exc()
|
| 256 |
+
return 1
|
| 257 |
+
|
| 258 |
+
return 0
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
if __name__ == "__main__":
|
| 262 |
+
sys.exit(main())
|
quickstart.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quick Start Guide - Metro Train Scheduling System
|
| 3 |
+
|
| 4 |
+
This script shows the basic usage patterns for the Metro Train Scheduling System.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from DataService import (
|
| 9 |
+
MetroDataGenerator,
|
| 10 |
+
MetroScheduleOptimizer,
|
| 11 |
+
ScheduleRequest
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def example_1_basic_data_generation():
|
| 16 |
+
"""Example 1: Generate basic metro data"""
|
| 17 |
+
print("\n" + "=" * 60)
|
| 18 |
+
print("EXAMPLE 1: Basic Data Generation")
|
| 19 |
+
print("=" * 60)
|
| 20 |
+
|
| 21 |
+
# Create generator for 25 trains
|
| 22 |
+
generator = MetroDataGenerator(num_trains=25, num_stations=25)
|
| 23 |
+
|
| 24 |
+
# Generate route
|
| 25 |
+
route = generator.generate_route("Aluva-Pettah Line")
|
| 26 |
+
print(f"\nRoute: {route.name}")
|
| 27 |
+
print(f"Distance: {route.total_distance_km} km")
|
| 28 |
+
print(f"Stations: {len(route.stations)}")
|
| 29 |
+
|
| 30 |
+
# Generate train health status
|
| 31 |
+
health_statuses = generator.generate_train_health_statuses()
|
| 32 |
+
print(f"\nGenerated health status for {len(health_statuses)} trains")
|
| 33 |
+
|
| 34 |
+
# Count by category
|
| 35 |
+
healthy = sum(1 for h in health_statuses if h.is_fully_healthy)
|
| 36 |
+
print(f" - Fully healthy: {healthy}")
|
| 37 |
+
print(f" - Need attention: {len(health_statuses) - healthy}")
|
| 38 |
+
|
| 39 |
+
return generator, route, health_statuses
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def example_2_simple_schedule():
|
| 43 |
+
"""Example 2: Generate a simple schedule"""
|
| 44 |
+
print("\n" + "=" * 60)
|
| 45 |
+
print("EXAMPLE 2: Generate Simple Schedule")
|
| 46 |
+
print("=" * 60)
|
| 47 |
+
|
| 48 |
+
# Setup
|
| 49 |
+
generator = MetroDataGenerator(num_trains=30)
|
| 50 |
+
route = generator.generate_route()
|
| 51 |
+
health_statuses = generator.generate_train_health_statuses()
|
| 52 |
+
|
| 53 |
+
# Create optimizer
|
| 54 |
+
optimizer = MetroScheduleOptimizer(
|
| 55 |
+
date="2025-10-25",
|
| 56 |
+
num_trains=30,
|
| 57 |
+
route=route,
|
| 58 |
+
train_health=health_statuses
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Generate schedule
|
| 62 |
+
schedule = optimizer.optimize_schedule(
|
| 63 |
+
min_service_trains=22,
|
| 64 |
+
min_standby=3
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
print(f"\nSchedule ID: {schedule.schedule_id}")
|
| 68 |
+
print(f"Valid: {schedule.valid_from} to {schedule.valid_until}")
|
| 69 |
+
print(f"\nFleet Status:")
|
| 70 |
+
print(f" - In service: {schedule.fleet_summary.revenue_service}")
|
| 71 |
+
print(f" - Standby: {schedule.fleet_summary.standby}")
|
| 72 |
+
print(f" - Maintenance: {schedule.fleet_summary.maintenance}")
|
| 73 |
+
print(f" - Cleaning: {schedule.fleet_summary.cleaning}")
|
| 74 |
+
|
| 75 |
+
return schedule
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def example_3_detailed_schedule():
|
| 79 |
+
"""Example 3: Generate schedule with custom parameters"""
|
| 80 |
+
print("\n" + "=" * 60)
|
| 81 |
+
print("EXAMPLE 3: Custom Schedule Parameters")
|
| 82 |
+
print("=" * 60)
|
| 83 |
+
|
| 84 |
+
generator = MetroDataGenerator(num_trains=35)
|
| 85 |
+
route = generator.generate_route()
|
| 86 |
+
health_statuses = generator.generate_train_health_statuses()
|
| 87 |
+
|
| 88 |
+
optimizer = MetroScheduleOptimizer(
|
| 89 |
+
date=datetime.now().strftime("%Y-%m-%d"),
|
| 90 |
+
num_trains=35,
|
| 91 |
+
route=route,
|
| 92 |
+
train_health=health_statuses,
|
| 93 |
+
depot_name="Custom_Depot"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Custom optimization parameters
|
| 97 |
+
schedule = optimizer.optimize_schedule(
|
| 98 |
+
min_service_trains=25, # More trains in service
|
| 99 |
+
min_standby=5, # More standby trains
|
| 100 |
+
max_daily_km=280 # Lower km limit per train
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
print(f"\nSchedule optimized with custom parameters:")
|
| 104 |
+
print(f" - Total planned km: {schedule.optimization_metrics.total_planned_km:,}")
|
| 105 |
+
print(f" - Avg readiness: {schedule.optimization_metrics.avg_readiness_score:.2f}")
|
| 106 |
+
print(f" - Runtime: {schedule.optimization_metrics.optimization_runtime_ms} ms")
|
| 107 |
+
|
| 108 |
+
return schedule
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def example_4_train_details():
|
| 112 |
+
"""Example 4: Access detailed train information"""
|
| 113 |
+
print("\n" + "=" * 60)
|
| 114 |
+
print("EXAMPLE 4: Detailed Train Information")
|
| 115 |
+
print("=" * 60)
|
| 116 |
+
|
| 117 |
+
generator = MetroDataGenerator(num_trains=30)
|
| 118 |
+
route = generator.generate_route()
|
| 119 |
+
health_statuses = generator.generate_train_health_statuses()
|
| 120 |
+
|
| 121 |
+
optimizer = MetroScheduleOptimizer(
|
| 122 |
+
date="2025-10-25",
|
| 123 |
+
num_trains=30,
|
| 124 |
+
route=route,
|
| 125 |
+
train_health=health_statuses
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
schedule = optimizer.optimize_schedule()
|
| 129 |
+
|
| 130 |
+
# Find first train in revenue service
|
| 131 |
+
service_train = next(
|
| 132 |
+
(t for t in schedule.trainsets if t.status.value == "REVENUE_SERVICE"),
|
| 133 |
+
None
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
if service_train:
|
| 137 |
+
print(f"\nTrain: {service_train.trainset_id}")
|
| 138 |
+
print(f"Status: {service_train.status.value}")
|
| 139 |
+
print(f"Duty: {service_train.assigned_duty}")
|
| 140 |
+
print(f"Daily km: {service_train.daily_km_allocation} km")
|
| 141 |
+
print(f"Readiness: {service_train.readiness_score:.2f}")
|
| 142 |
+
|
| 143 |
+
if service_train.service_blocks:
|
| 144 |
+
print(f"\nService Blocks: {len(service_train.service_blocks)}")
|
| 145 |
+
for i, block in enumerate(service_train.service_blocks[:3], 1):
|
| 146 |
+
print(f" {i}. {block.origin} → {block.destination}")
|
| 147 |
+
print(f" Depart: {block.departure_time}, Trips: {block.trip_count}")
|
| 148 |
+
|
| 149 |
+
print(f"\nFitness Certificates:")
|
| 150 |
+
certs = service_train.fitness_certificates
|
| 151 |
+
print(f" - Rolling Stock: {certs.rolling_stock.status.value}")
|
| 152 |
+
print(f" - Signalling: {certs.signalling.status.value}")
|
| 153 |
+
print(f" - Telecom: {certs.telecom.status.value}")
|
| 154 |
+
|
| 155 |
+
if service_train.branding and service_train.branding.advertiser != "NONE":
|
| 156 |
+
print(f"\nBranding:")
|
| 157 |
+
print(f" - Advertiser: {service_train.branding.advertiser}")
|
| 158 |
+
print(f" - Priority: {service_train.branding.exposure_priority}")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def example_5_schedule_request_model():
|
| 162 |
+
"""Example 5: Using ScheduleRequest model (for API)"""
|
| 163 |
+
print("\n" + "=" * 60)
|
| 164 |
+
print("EXAMPLE 5: Schedule Request Model")
|
| 165 |
+
print("=" * 60)
|
| 166 |
+
|
| 167 |
+
# Create a request (as would be done via API)
|
| 168 |
+
request = ScheduleRequest(
|
| 169 |
+
date="2025-10-25",
|
| 170 |
+
num_trains=30,
|
| 171 |
+
num_stations=25,
|
| 172 |
+
route_name="Aluva-Pettah Line",
|
| 173 |
+
depot_name="Muttom_Depot",
|
| 174 |
+
min_service_trains=22,
|
| 175 |
+
min_standby_trains=3,
|
| 176 |
+
max_daily_km_per_train=300,
|
| 177 |
+
balance_mileage=True,
|
| 178 |
+
prioritize_branding=True
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
print(f"\nSchedule Request:")
|
| 182 |
+
print(f" - Date: {request.date}")
|
| 183 |
+
print(f" - Trains: {request.num_trains}")
|
| 184 |
+
print(f" - Stations: {request.num_stations}")
|
| 185 |
+
print(f" - Min service: {request.min_service_trains}")
|
| 186 |
+
print(f" - Max daily km: {request.max_daily_km_per_train}")
|
| 187 |
+
|
| 188 |
+
# This request can be sent to the API:
|
| 189 |
+
# POST /api/v1/generate with request.model_dump() as JSON
|
| 190 |
+
|
| 191 |
+
return request
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def example_6_save_schedule():
|
| 195 |
+
"""Example 6: Save schedule to JSON file"""
|
| 196 |
+
print("\n" + "=" * 60)
|
| 197 |
+
print("EXAMPLE 6: Save Schedule to File")
|
| 198 |
+
print("=" * 60)
|
| 199 |
+
|
| 200 |
+
import json
|
| 201 |
+
|
| 202 |
+
generator = MetroDataGenerator(num_trains=25)
|
| 203 |
+
route = generator.generate_route()
|
| 204 |
+
health_statuses = generator.generate_train_health_statuses()
|
| 205 |
+
|
| 206 |
+
optimizer = MetroScheduleOptimizer(
|
| 207 |
+
date="2025-10-25",
|
| 208 |
+
num_trains=25,
|
| 209 |
+
route=route,
|
| 210 |
+
train_health=health_statuses
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
schedule = optimizer.optimize_schedule()
|
| 214 |
+
|
| 215 |
+
# Convert to dict and save
|
| 216 |
+
schedule_dict = schedule.model_dump()
|
| 217 |
+
|
| 218 |
+
filename = f"schedule_{schedule.schedule_id}.json"
|
| 219 |
+
with open(filename, 'w') as f:
|
| 220 |
+
json.dump(schedule_dict, f, indent=2, default=str)
|
| 221 |
+
|
| 222 |
+
print(f"\nSchedule saved to: {filename}")
|
| 223 |
+
print(f"Contains {len(schedule_dict['trainsets'])} trainsets")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def main():
|
| 227 |
+
"""Run all examples"""
|
| 228 |
+
print("\n" + "🚇" * 30)
|
| 229 |
+
print(" METRO TRAIN SCHEDULING - QUICK START EXAMPLES")
|
| 230 |
+
print("🚇" * 30)
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
# Run examples
|
| 234 |
+
example_1_basic_data_generation()
|
| 235 |
+
example_2_simple_schedule()
|
| 236 |
+
example_3_detailed_schedule()
|
| 237 |
+
example_4_train_details()
|
| 238 |
+
example_5_schedule_request_model()
|
| 239 |
+
example_6_save_schedule()
|
| 240 |
+
|
| 241 |
+
print("\n" + "=" * 60)
|
| 242 |
+
print("ALL EXAMPLES COMPLETED SUCCESSFULLY!")
|
| 243 |
+
print("=" * 60)
|
| 244 |
+
print("\nNext steps:")
|
| 245 |
+
print(" 1. Run 'python demo_schedule.py' for a comprehensive demo")
|
| 246 |
+
print(" 2. Run 'python run_api.py' to start the FastAPI service")
|
| 247 |
+
print(" 3. Visit http://localhost:8000/docs for API documentation")
|
| 248 |
+
print()
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
print(f"\n❌ Error: {e}")
|
| 252 |
+
import traceback
|
| 253 |
+
traceback.print_exc()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
main()
|
requirements.txt
CHANGED
|
@@ -1 +1,5 @@
|
|
| 1 |
-
ortools==9.14.6206
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ortools==9.14.6206
|
| 2 |
+
fastapi==0.104.1
|
| 3 |
+
uvicorn[standard]==0.24.0
|
| 4 |
+
pydantic==2.5.0
|
| 5 |
+
python-multipart==0.0.6
|
run_api.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Startup script for Metro Train Scheduling API
|
| 4 |
+
"""
|
| 5 |
+
import uvicorn
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Add parent directory to path
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 11 |
+
|
| 12 |
+
if __name__ == "__main__":
|
| 13 |
+
print("=" * 60)
|
| 14 |
+
print("Metro Train Scheduling API")
|
| 15 |
+
print("=" * 60)
|
| 16 |
+
print()
|
| 17 |
+
print("Starting FastAPI server...")
|
| 18 |
+
print("API Documentation: http://localhost:8000/docs")
|
| 19 |
+
print("Alternative Docs: http://localhost:8000/redoc")
|
| 20 |
+
print()
|
| 21 |
+
print("Example endpoints:")
|
| 22 |
+
print(" - GET /health")
|
| 23 |
+
print(" - GET /api/v1/schedule/example")
|
| 24 |
+
print(" - POST /api/v1/generate")
|
| 25 |
+
print(" - POST /api/v1/generate/quick?date=2025-10-25&num_trains=30")
|
| 26 |
+
print()
|
| 27 |
+
print("Press CTRL+C to stop the server")
|
| 28 |
+
print("=" * 60)
|
| 29 |
+
print()
|
| 30 |
+
|
| 31 |
+
uvicorn.run(
|
| 32 |
+
"DataService.api:app",
|
| 33 |
+
host="0.0.0.0",
|
| 34 |
+
port=8000,
|
| 35 |
+
reload=True,
|
| 36 |
+
log_level="info"
|
| 37 |
+
)
|
test_system.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple Test Script - Verify Metro Scheduling System
|
| 3 |
+
Tests core functionality without requiring full API setup
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import traceback
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_imports():
|
| 10 |
+
"""Test that all modules can be imported"""
|
| 11 |
+
print("Testing imports...")
|
| 12 |
+
try:
|
| 13 |
+
from DataService import metro_models
|
| 14 |
+
from DataService import metro_data_generator
|
| 15 |
+
from DataService import schedule_optimizer
|
| 16 |
+
print(" ✓ DataService modules imported successfully")
|
| 17 |
+
return True
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print(f" ✗ Import failed: {e}")
|
| 20 |
+
traceback.print_exc()
|
| 21 |
+
return False
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_data_generation():
|
| 25 |
+
"""Test data generation"""
|
| 26 |
+
print("\nTesting data generation...")
|
| 27 |
+
try:
|
| 28 |
+
from DataService.metro_data_generator import MetroDataGenerator
|
| 29 |
+
|
| 30 |
+
generator = MetroDataGenerator(num_trains=10, num_stations=10)
|
| 31 |
+
print(f" ✓ Generator created for {len(generator.trainset_ids)} trains")
|
| 32 |
+
|
| 33 |
+
# Test route generation
|
| 34 |
+
route = generator.generate_route()
|
| 35 |
+
print(f" ✓ Route generated: {route.name} with {len(route.stations)} stations")
|
| 36 |
+
|
| 37 |
+
# Test train health
|
| 38 |
+
health = generator.generate_train_health_statuses()
|
| 39 |
+
print(f" ✓ Generated health status for {len(health)} trains")
|
| 40 |
+
|
| 41 |
+
# Test certificates
|
| 42 |
+
certs = generator.generate_fitness_certificates("TS-001")
|
| 43 |
+
print(f" ✓ Generated fitness certificates")
|
| 44 |
+
|
| 45 |
+
return True
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f" ✗ Data generation failed: {e}")
|
| 48 |
+
traceback.print_exc()
|
| 49 |
+
return False
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_schedule_optimization():
|
| 53 |
+
"""Test schedule optimization"""
|
| 54 |
+
print("\nTesting schedule optimization...")
|
| 55 |
+
try:
|
| 56 |
+
from DataService.metro_data_generator import MetroDataGenerator
|
| 57 |
+
from DataService.schedule_optimizer import MetroScheduleOptimizer
|
| 58 |
+
from datetime import datetime
|
| 59 |
+
|
| 60 |
+
# Setup
|
| 61 |
+
generator = MetroDataGenerator(num_trains=15, num_stations=15)
|
| 62 |
+
route = generator.generate_route()
|
| 63 |
+
health = generator.generate_train_health_statuses()
|
| 64 |
+
|
| 65 |
+
# Create optimizer
|
| 66 |
+
optimizer = MetroScheduleOptimizer(
|
| 67 |
+
date=datetime.now().strftime("%Y-%m-%d"),
|
| 68 |
+
num_trains=15,
|
| 69 |
+
route=route,
|
| 70 |
+
train_health=health
|
| 71 |
+
)
|
| 72 |
+
print(f" ✓ Optimizer created")
|
| 73 |
+
|
| 74 |
+
# Generate schedule
|
| 75 |
+
schedule = optimizer.optimize_schedule(min_service_trains=10, min_standby=2)
|
| 76 |
+
print(f" ✓ Schedule generated: {schedule.schedule_id}")
|
| 77 |
+
print(f" - Trains in service: {schedule.fleet_summary.revenue_service}")
|
| 78 |
+
print(f" - Total planned km: {schedule.optimization_metrics.total_planned_km}")
|
| 79 |
+
print(f" - Optimization time: {schedule.optimization_metrics.optimization_runtime_ms} ms")
|
| 80 |
+
|
| 81 |
+
return True
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f" ✗ Schedule optimization failed: {e}")
|
| 84 |
+
traceback.print_exc()
|
| 85 |
+
return False
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_models():
|
| 89 |
+
"""Test Pydantic models"""
|
| 90 |
+
print("\nTesting data models...")
|
| 91 |
+
try:
|
| 92 |
+
from DataService.metro_models import (
|
| 93 |
+
ScheduleRequest, TrainHealthStatus, Route, Station
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Test ScheduleRequest
|
| 97 |
+
request = ScheduleRequest(
|
| 98 |
+
date="2025-10-25",
|
| 99 |
+
num_trains=25,
|
| 100 |
+
num_stations=25
|
| 101 |
+
)
|
| 102 |
+
print(f" ✓ ScheduleRequest model validated")
|
| 103 |
+
|
| 104 |
+
# Test Station
|
| 105 |
+
station = Station(
|
| 106 |
+
station_id="STN-001",
|
| 107 |
+
name="Test Station",
|
| 108 |
+
sequence=1,
|
| 109 |
+
distance_from_origin_km=0.0
|
| 110 |
+
)
|
| 111 |
+
print(f" ✓ Station model validated")
|
| 112 |
+
|
| 113 |
+
return True
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f" ✗ Model validation failed: {e}")
|
| 116 |
+
traceback.print_exc()
|
| 117 |
+
return False
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_json_export():
|
| 121 |
+
"""Test JSON export"""
|
| 122 |
+
print("\nTesting JSON export...")
|
| 123 |
+
try:
|
| 124 |
+
import json
|
| 125 |
+
from DataService.metro_data_generator import MetroDataGenerator
|
| 126 |
+
from DataService.schedule_optimizer import MetroScheduleOptimizer
|
| 127 |
+
from datetime import datetime
|
| 128 |
+
|
| 129 |
+
generator = MetroDataGenerator(num_trains=10, num_stations=10)
|
| 130 |
+
route = generator.generate_route()
|
| 131 |
+
health = generator.generate_train_health_statuses()
|
| 132 |
+
|
| 133 |
+
optimizer = MetroScheduleOptimizer(
|
| 134 |
+
date=datetime.now().strftime("%Y-%m-%d"),
|
| 135 |
+
num_trains=10,
|
| 136 |
+
route=route,
|
| 137 |
+
train_health=health
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
schedule = optimizer.optimize_schedule()
|
| 141 |
+
|
| 142 |
+
# Convert to dict and save
|
| 143 |
+
schedule_dict = schedule.model_dump()
|
| 144 |
+
|
| 145 |
+
# Try to serialize to JSON
|
| 146 |
+
json_str = json.dumps(schedule_dict, indent=2, default=str)
|
| 147 |
+
|
| 148 |
+
print(f" ✓ Schedule exported to JSON ({len(json_str)} chars)")
|
| 149 |
+
print(f" - Contains {len(schedule_dict['trainsets'])} trainsets")
|
| 150 |
+
|
| 151 |
+
return True
|
| 152 |
+
except Exception as e:
|
| 153 |
+
print(f" ✗ JSON export failed: {e}")
|
| 154 |
+
traceback.print_exc()
|
| 155 |
+
return False
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def main():
|
| 159 |
+
"""Run all tests"""
|
| 160 |
+
print("=" * 70)
|
| 161 |
+
print(" METRO SCHEDULING SYSTEM - VERIFICATION TESTS")
|
| 162 |
+
print("=" * 70)
|
| 163 |
+
|
| 164 |
+
tests = [
|
| 165 |
+
("Imports", test_imports),
|
| 166 |
+
("Data Generation", test_data_generation),
|
| 167 |
+
("Schedule Optimization", test_schedule_optimization),
|
| 168 |
+
("Data Models", test_models),
|
| 169 |
+
("JSON Export", test_json_export)
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
results = []
|
| 173 |
+
|
| 174 |
+
for name, test_func in tests:
|
| 175 |
+
try:
|
| 176 |
+
result = test_func()
|
| 177 |
+
results.append((name, result))
|
| 178 |
+
except Exception as e:
|
| 179 |
+
print(f"\n✗ {name} crashed: {e}")
|
| 180 |
+
results.append((name, False))
|
| 181 |
+
|
| 182 |
+
# Summary
|
| 183 |
+
print("\n" + "=" * 70)
|
| 184 |
+
print(" TEST SUMMARY")
|
| 185 |
+
print("=" * 70)
|
| 186 |
+
|
| 187 |
+
passed = sum(1 for _, result in results if result)
|
| 188 |
+
total = len(results)
|
| 189 |
+
|
| 190 |
+
for name, result in results:
|
| 191 |
+
status = "✓ PASS" if result else "✗ FAIL"
|
| 192 |
+
print(f" {status}: {name}")
|
| 193 |
+
|
| 194 |
+
print("\n" + "-" * 70)
|
| 195 |
+
print(f" Results: {passed}/{total} tests passed")
|
| 196 |
+
|
| 197 |
+
if passed == total:
|
| 198 |
+
print("\n 🎉 All tests passed! System is ready to use.")
|
| 199 |
+
print("\n Next steps:")
|
| 200 |
+
print(" 1. Run: python demo_schedule.py")
|
| 201 |
+
print(" 2. Run: python run_api.py")
|
| 202 |
+
print(" 3. Visit: http://localhost:8000/docs")
|
| 203 |
+
else:
|
| 204 |
+
print("\n ⚠️ Some tests failed. Please check the errors above.")
|
| 205 |
+
print(" Make sure all dependencies are installed:")
|
| 206 |
+
print(" pip install -r requirements.txt")
|
| 207 |
+
|
| 208 |
+
print("=" * 70)
|
| 209 |
+
|
| 210 |
+
return 0 if passed == total else 1
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
sys.exit(main())
|