|
|
""" |
|
|
FastAPI Service for Metro Train Schedule Generation |
|
|
Provides endpoints for synthetic data generation and schedule optimization |
|
|
""" |
|
|
import sys |
|
|
import os |
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
|
|
|
from fastapi import FastAPI, HTTPException |
|
|
from fastapi.responses import JSONResponse |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from pydantic import ValidationError |
|
|
from datetime import datetime |
|
|
import logging |
|
|
|
|
|
from DataService.metro_models import ( |
|
|
DaySchedule, ScheduleRequest, Route, TrainHealthStatus |
|
|
) |
|
|
from DataService.metro_data_generator import MetroDataGenerator |
|
|
from DataService.schedule_optimizer import MetroScheduleOptimizer |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="Metro Train Scheduling API", |
|
|
description="Generate synthetic metro data and optimize daily train schedules", |
|
|
version="1.0.0", |
|
|
docs_url="/docs", |
|
|
redoc_url="/redoc" |
|
|
) |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
@app.get("/") |
|
|
async def root(): |
|
|
"""Root endpoint with API information""" |
|
|
return { |
|
|
"service": "Metro Train Scheduling API", |
|
|
"version": "1.0.0", |
|
|
"endpoints": { |
|
|
"schedule": "/api/v1/schedule", |
|
|
"generate": "/api/v1/generate", |
|
|
"health": "/health", |
|
|
"docs": "/docs" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@app.get("/health") |
|
|
async def health_check(): |
|
|
"""Health check endpoint""" |
|
|
return { |
|
|
"status": "healthy", |
|
|
"timestamp": datetime.now().isoformat(), |
|
|
"service": "metro-scheduling-api" |
|
|
} |
|
|
|
|
|
|
|
|
@app.post("/api/v1/generate", response_model=DaySchedule) |
|
|
async def generate_schedule(request: ScheduleRequest): |
|
|
""" |
|
|
Generate optimized daily train schedule |
|
|
|
|
|
Args: |
|
|
request: Schedule request with date, train count, and optimization parameters |
|
|
|
|
|
Returns: |
|
|
DaySchedule: Complete optimized schedule with all trainset assignments |
|
|
|
|
|
Example: |
|
|
POST /api/v1/generate |
|
|
{ |
|
|
"date": "2025-10-25", |
|
|
"num_trains": 30, |
|
|
"num_stations": 25, |
|
|
"min_service_trains": 22, |
|
|
"min_standby_trains": 3 |
|
|
} |
|
|
""" |
|
|
try: |
|
|
logger.info(f"Generating schedule for {request.date} with {request.num_trains} trains") |
|
|
|
|
|
|
|
|
generator = MetroDataGenerator( |
|
|
num_trains=request.num_trains, |
|
|
num_stations=request.num_stations |
|
|
) |
|
|
|
|
|
|
|
|
route = generator.generate_route(request.route_name) |
|
|
logger.info(f"Generated route: {route.name} with {len(route.stations)} stations") |
|
|
|
|
|
|
|
|
if request.train_health_overrides: |
|
|
train_health = request.train_health_overrides |
|
|
else: |
|
|
train_health = generator.generate_train_health_statuses() |
|
|
|
|
|
logger.info(f"Train health data: {len(train_health)} trains initialized") |
|
|
|
|
|
|
|
|
optimizer = MetroScheduleOptimizer( |
|
|
date=request.date, |
|
|
num_trains=request.num_trains, |
|
|
route=route, |
|
|
train_health=train_health, |
|
|
depot_name=request.depot_name |
|
|
) |
|
|
|
|
|
|
|
|
schedule = optimizer.optimize_schedule( |
|
|
min_service_trains=request.min_service_trains, |
|
|
min_standby=request.min_standby_trains, |
|
|
max_daily_km=request.max_daily_km_per_train |
|
|
) |
|
|
|
|
|
logger.info( |
|
|
f"Schedule generated: {schedule.schedule_id}, " |
|
|
f"{schedule.fleet_summary.revenue_service} trains in service, " |
|
|
f"{schedule.optimization_metrics.total_planned_km} km planned" |
|
|
) |
|
|
|
|
|
return schedule |
|
|
|
|
|
except ValidationError as e: |
|
|
logger.error(f"Validation error: {e}") |
|
|
raise HTTPException(status_code=422, detail=str(e)) |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating schedule: {e}", exc_info=True) |
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") |
|
|
|
|
|
|
|
|
@app.post("/api/v1/generate/quick") |
|
|
async def generate_quick_schedule( |
|
|
date: str = "2025-10-25", |
|
|
num_trains: int = 25, |
|
|
num_stations: int = 25 |
|
|
): |
|
|
""" |
|
|
Quick schedule generation with default parameters |
|
|
|
|
|
Query Parameters: |
|
|
- date: Schedule date (YYYY-MM-DD) |
|
|
- num_trains: Number of trains in fleet (default: 25) |
|
|
- num_stations: Number of stations on route (default: 25) |
|
|
""" |
|
|
request = ScheduleRequest( |
|
|
date=date, |
|
|
num_trains=num_trains, |
|
|
num_stations=num_stations |
|
|
) |
|
|
return await generate_schedule(request) |
|
|
|
|
|
|
|
|
@app.get("/api/v1/route/{num_stations}") |
|
|
async def get_route_info(num_stations: int = 25): |
|
|
""" |
|
|
Get metro route information |
|
|
|
|
|
Args: |
|
|
num_stations: Number of stations to include (default: 25) |
|
|
|
|
|
Returns: |
|
|
Route information with all stations |
|
|
""" |
|
|
try: |
|
|
generator = MetroDataGenerator(num_stations=num_stations) |
|
|
route = generator.generate_route() |
|
|
|
|
|
return { |
|
|
"route": route.model_dump(), |
|
|
"one_way_time_minutes": int((route.total_distance_km / route.avg_speed_kmh) * 60), |
|
|
"round_trip_time_minutes": int((route.total_distance_km / route.avg_speed_kmh) * 60 * 2) + route.turnaround_time_minutes * 2 |
|
|
} |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating route: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@app.get("/api/v1/trains/health/{num_trains}") |
|
|
async def get_train_health(num_trains: int = 25): |
|
|
""" |
|
|
Generate train health status data |
|
|
|
|
|
Args: |
|
|
num_trains: Number of trains (default: 25) |
|
|
|
|
|
Returns: |
|
|
List of train health statuses |
|
|
""" |
|
|
try: |
|
|
generator = MetroDataGenerator(num_trains=num_trains) |
|
|
health_data = generator.generate_train_health_statuses() |
|
|
|
|
|
summary = { |
|
|
"total": len(health_data), |
|
|
"fully_healthy": sum(1 for h in health_data if h.is_fully_healthy), |
|
|
"partial": sum(1 for h in health_data if not h.is_fully_healthy and h.available_hours), |
|
|
"unavailable": sum(1 for h in health_data if not h.is_fully_healthy and not h.available_hours) |
|
|
} |
|
|
|
|
|
return { |
|
|
"summary": summary, |
|
|
"trains": [h.dict() for h in health_data] |
|
|
} |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating train health: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@app.get("/api/v1/depot/layout") |
|
|
async def get_depot_layout(): |
|
|
"""Get depot bay layout information""" |
|
|
try: |
|
|
generator = MetroDataGenerator() |
|
|
layout = generator.generate_depot_layout() |
|
|
|
|
|
return { |
|
|
"depot": "Muttom_Depot", |
|
|
"layout": layout, |
|
|
"total_bays": sum(len(bays) for bays in layout.values()) |
|
|
} |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating depot layout: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@app.get("/api/v1/schedule/example") |
|
|
async def get_example_schedule(): |
|
|
"""Get an example schedule for demonstration""" |
|
|
request = ScheduleRequest( |
|
|
date=datetime.now().strftime("%Y-%m-%d"), |
|
|
num_trains=30, |
|
|
num_stations=25, |
|
|
min_service_trains=22, |
|
|
min_standby_trains=4 |
|
|
) |
|
|
return await generate_schedule(request) |
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(404) |
|
|
async def not_found_handler(request, exc): |
|
|
return JSONResponse( |
|
|
status_code=404, |
|
|
content={ |
|
|
"error": "Not Found", |
|
|
"message": "The requested resource was not found", |
|
|
"path": str(request.url) |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
@app.exception_handler(500) |
|
|
async def internal_error_handler(request, exc): |
|
|
logger.error(f"Internal server error: {exc}", exc_info=True) |
|
|
return JSONResponse( |
|
|
status_code=500, |
|
|
content={ |
|
|
"error": "Internal Server Error", |
|
|
"message": "An unexpected error occurred" |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") |
|
|
|