Arpit-Bansal commited on
Commit
aed9c8c
·
1 Parent(s): a9ae8ce

api for scheduling exposed

Browse files
api/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API Package for Metro Train Scheduling System
3
+
4
+ Provides separate API services for:
5
+ - DataService: Simple schedule generation (port 8000)
6
+ - GreedyOptim: Advanced optimization with customizable input (port 8001)
7
+ """
8
+
9
+ __version__ = '2.0.0'
api/greedyoptim_api.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Service for GreedyOptim Scheduling
3
+ Exposes greedyOptim functionality with customizable input data
4
+ """
5
+ from fastapi import FastAPI, HTTPException
6
+ from fastapi.responses import JSONResponse
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel, Field
9
+ from typing import Dict, List, Any, Optional
10
+ from datetime import datetime
11
+ import logging
12
+ import sys
13
+ import os
14
+
15
+ # Add parent directory to path
16
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ # Import greedyOptim components
19
+ from greedyOptim.scheduler import optimize_trainset_schedule, compare_optimization_methods
20
+ from greedyOptim.models import OptimizationConfig, OptimizationResult
21
+ from greedyOptim.error_handling import DataValidator
22
+
23
+ # Import DataService for synthetic data generation (optional)
24
+ from DataService.enhanced_generator import EnhancedMetroDataGenerator
25
+
26
+ # Configure logging
27
+ logging.basicConfig(level=logging.INFO)
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Create FastAPI app
31
+ app = FastAPI(
32
+ title="GreedyOptim Scheduling API",
33
+ description="Advanced train scheduling optimization using genetic algorithms, PSO, CMA-ES, and more",
34
+ version="2.0.0",
35
+ docs_url="/docs",
36
+ redoc_url="/redoc"
37
+ )
38
+
39
+ # Add CORS middleware
40
+ app.add_middleware(
41
+ CORSMiddleware,
42
+ allow_origins=["*"],
43
+ allow_credentials=True,
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+
48
+
49
+ # ============================================================================
50
+ # Request/Response Models
51
+ # ============================================================================
52
+
53
+ class TrainsetStatusInput(BaseModel):
54
+ """Single trainset operational status"""
55
+ trainset_id: str
56
+ operational_status: str = Field(..., description="Available, In-Service, Maintenance, Standby, Out-of-Order")
57
+ last_maintenance_date: Optional[str] = None
58
+ total_mileage_km: Optional[float] = None
59
+ age_years: Optional[float] = None
60
+
61
+
62
+ class FitnessCertificateInput(BaseModel):
63
+ """Fitness certificate for a trainset"""
64
+ trainset_id: str
65
+ department: str = Field(..., description="Safety, Operations, Technical, Electrical, Mechanical")
66
+ status: str = Field(..., description="Valid, Expired, Expiring-Soon, Suspended")
67
+ issue_date: Optional[str] = None
68
+ expiry_date: Optional[str] = None
69
+
70
+
71
+ class JobCardInput(BaseModel):
72
+ """Job card/work order for trainset"""
73
+ trainset_id: str
74
+ job_id: str
75
+ priority: str = Field(..., description="Critical, High, Medium, Low")
76
+ status: str = Field(..., description="Open, In-Progress, Closed, Pending-Parts")
77
+ description: Optional[str] = None
78
+ estimated_hours: Optional[float] = None
79
+
80
+
81
+ class ComponentHealthInput(BaseModel):
82
+ """Component health status"""
83
+ trainset_id: str
84
+ component: str = Field(..., description="Brakes, HVAC, Doors, Propulsion, etc.")
85
+ status: str = Field(..., description="Good, Fair, Warning, Critical")
86
+ wear_level: Optional[float] = Field(None, ge=0, le=100)
87
+ last_inspection: Optional[str] = None
88
+
89
+
90
+ class OptimizationConfigInput(BaseModel):
91
+ """Configuration for optimization algorithm"""
92
+ required_service_trains: Optional[int] = Field(15, description="Minimum trains required in service")
93
+ min_standby: Optional[int] = Field(2, description="Minimum standby trains")
94
+
95
+ # Genetic Algorithm parameters
96
+ population_size: Optional[int] = Field(50, ge=10, le=200)
97
+ generations: Optional[int] = Field(100, ge=10, le=1000)
98
+ mutation_rate: Optional[float] = Field(0.1, ge=0.0, le=1.0)
99
+ crossover_rate: Optional[float] = Field(0.8, ge=0.0, le=1.0)
100
+ elite_size: Optional[int] = Field(5, ge=1)
101
+
102
+
103
+ class ScheduleOptimizationRequest(BaseModel):
104
+ """Request for schedule optimization"""
105
+ trainset_status: List[TrainsetStatusInput]
106
+ fitness_certificates: List[FitnessCertificateInput]
107
+ job_cards: List[JobCardInput]
108
+ component_health: List[ComponentHealthInput]
109
+
110
+ # Optional metadata
111
+ metadata: Optional[Dict[str, Any]] = None
112
+ date: Optional[str] = Field(None, description="Date for schedule (YYYY-MM-DD)")
113
+
114
+ # Optimization configuration
115
+ config: Optional[OptimizationConfigInput] = None
116
+ method: str = Field("ga", description="Optimization method: ga, cmaes, pso, sa, nsga2, adaptive, ensemble")
117
+
118
+ # Optional additional data
119
+ branding_contracts: Optional[List[Dict[str, Any]]] = None
120
+ maintenance_schedule: Optional[List[Dict[str, Any]]] = None
121
+ performance_metrics: Optional[List[Dict[str, Any]]] = None
122
+
123
+
124
+ class CompareMethodsRequest(BaseModel):
125
+ """Request to compare multiple optimization methods"""
126
+ trainset_status: List[TrainsetStatusInput]
127
+ fitness_certificates: List[FitnessCertificateInput]
128
+ job_cards: List[JobCardInput]
129
+ component_health: List[ComponentHealthInput]
130
+
131
+ metadata: Optional[Dict[str, Any]] = None
132
+ date: Optional[str] = None
133
+ config: Optional[OptimizationConfigInput] = None
134
+ methods: List[str] = Field(["ga", "pso", "cmaes"], description="Methods to compare")
135
+
136
+
137
+ class SyntheticDataRequest(BaseModel):
138
+ """Request to generate synthetic data"""
139
+ num_trainsets: int = Field(25, ge=5, le=100, description="Number of trainsets to generate")
140
+ maintenance_rate: float = Field(0.1, ge=0.0, le=0.5, description="Percentage in maintenance")
141
+ availability_rate: float = Field(0.8, ge=0.5, le=1.0, description="Percentage available for service")
142
+
143
+
144
+ class ScheduleOptimizationResponse(BaseModel):
145
+ """Response from optimization"""
146
+ success: bool
147
+ method: str
148
+ fitness_score: float
149
+
150
+ # Schedule allocation
151
+ service_trains: List[str]
152
+ standby_trains: List[str]
153
+ maintenance_trains: List[str]
154
+ unavailable_trains: List[str]
155
+
156
+ # Metrics
157
+ num_service: int
158
+ num_standby: int
159
+ num_maintenance: int
160
+ num_unavailable: int
161
+
162
+ # Detailed scores
163
+ service_score: float
164
+ standby_score: float
165
+ health_score: float
166
+ certificate_score: float
167
+
168
+ # Metadata
169
+ execution_time_seconds: Optional[float] = None
170
+ timestamp: str
171
+ constraints_satisfied: bool
172
+ warnings: Optional[List[str]] = None
173
+
174
+
175
+ # ============================================================================
176
+ # Helper Functions
177
+ # ============================================================================
178
+
179
+ def convert_pydantic_to_dict(request: ScheduleOptimizationRequest) -> Dict[str, Any]:
180
+ """Convert Pydantic request model to dict format expected by greedyOptim"""
181
+ data = {
182
+ "trainset_status": [ts.dict() for ts in request.trainset_status],
183
+ "fitness_certificates": [fc.dict() for fc in request.fitness_certificates],
184
+ "job_cards": [jc.dict() for jc in request.job_cards],
185
+ "component_health": [ch.dict() for ch in request.component_health],
186
+ "metadata": request.metadata or {
187
+ "generated_at": datetime.now().isoformat(),
188
+ "system": "Kochi Metro Rail",
189
+ "date": request.date or datetime.now().strftime("%Y-%m-%d")
190
+ }
191
+ }
192
+
193
+ # Add optional data if provided
194
+ if request.branding_contracts:
195
+ data["branding_contracts"] = request.branding_contracts
196
+ if request.maintenance_schedule:
197
+ data["maintenance_schedule"] = request.maintenance_schedule
198
+ if request.performance_metrics:
199
+ data["performance_metrics"] = request.performance_metrics
200
+
201
+ return data
202
+
203
+
204
+ def convert_config(config_input: Optional[OptimizationConfigInput]) -> OptimizationConfig:
205
+ """Convert Pydantic config to OptimizationConfig"""
206
+ if config_input is None:
207
+ return OptimizationConfig()
208
+
209
+ return OptimizationConfig(
210
+ required_service_trains=config_input.required_service_trains or 15,
211
+ min_standby=config_input.min_standby or 2,
212
+ population_size=config_input.population_size or 50,
213
+ generations=config_input.generations or 100,
214
+ mutation_rate=config_input.mutation_rate or 0.1,
215
+ crossover_rate=config_input.crossover_rate or 0.8,
216
+ elite_size=config_input.elite_size or 5
217
+ )
218
+
219
+
220
+ def convert_result_to_response(
221
+ result: OptimizationResult,
222
+ method: str,
223
+ execution_time: Optional[float] = None
224
+ ) -> ScheduleOptimizationResponse:
225
+ """Convert OptimizationResult to API response"""
226
+ # Extract objectives
227
+ objectives = result.objectives
228
+
229
+ # Determine unavailable trains (those not selected, standby, or maintenance)
230
+ all_trains = set(result.selected_trainsets + result.standby_trainsets + result.maintenance_trainsets)
231
+ unavailable = [] # We don't have this info in current result structure
232
+
233
+ return ScheduleOptimizationResponse(
234
+ success=True,
235
+ method=method,
236
+ fitness_score=result.fitness_score,
237
+ service_trains=result.selected_trainsets,
238
+ standby_trains=result.standby_trainsets,
239
+ maintenance_trains=result.maintenance_trainsets,
240
+ unavailable_trains=unavailable,
241
+ num_service=len(result.selected_trainsets),
242
+ num_standby=len(result.standby_trainsets),
243
+ num_maintenance=len(result.maintenance_trainsets),
244
+ num_unavailable=len(unavailable),
245
+ service_score=objectives.get('service', 0.0),
246
+ standby_score=objectives.get('standby', 0.0),
247
+ health_score=objectives.get('health', 0.0),
248
+ certificate_score=objectives.get('certificates', 0.0),
249
+ execution_time_seconds=execution_time,
250
+ timestamp=datetime.now().isoformat(),
251
+ constraints_satisfied=len(result.selected_trainsets) >= 10, # Basic check
252
+ warnings=None
253
+ )
254
+
255
+
256
+ # ============================================================================
257
+ # API Endpoints
258
+ # ============================================================================
259
+
260
+ @app.get("/")
261
+ async def root():
262
+ """Root endpoint with API information"""
263
+ return {
264
+ "service": "GreedyOptim Scheduling API",
265
+ "version": "2.0.0",
266
+ "description": "Advanced train scheduling optimization",
267
+ "endpoints": {
268
+ "POST /optimize": "Optimize schedule with custom data",
269
+ "POST /compare": "Compare multiple optimization methods",
270
+ "POST /generate-synthetic": "Generate synthetic test data",
271
+ "POST /validate": "Validate input data structure",
272
+ "GET /health": "Health check",
273
+ "GET /methods": "List available optimization methods",
274
+ "GET /docs": "Interactive API documentation"
275
+ }
276
+ }
277
+
278
+
279
+ @app.get("/health")
280
+ async def health_check():
281
+ """Health check endpoint"""
282
+ return {
283
+ "status": "healthy",
284
+ "timestamp": datetime.now().isoformat(),
285
+ "service": "greedyoptim-api"
286
+ }
287
+
288
+
289
+ @app.get("/methods")
290
+ async def list_methods():
291
+ """List available optimization methods"""
292
+ return {
293
+ "available_methods": {
294
+ "ga": {
295
+ "name": "Genetic Algorithm",
296
+ "description": "Evolutionary optimization using selection, crossover, and mutation",
297
+ "typical_time": "medium",
298
+ "solution_quality": "high"
299
+ },
300
+ "cmaes": {
301
+ "name": "CMA-ES",
302
+ "description": "Covariance Matrix Adaptation Evolution Strategy",
303
+ "typical_time": "medium-high",
304
+ "solution_quality": "very high"
305
+ },
306
+ "pso": {
307
+ "name": "Particle Swarm Optimization",
308
+ "description": "Swarm intelligence-based optimization",
309
+ "typical_time": "medium",
310
+ "solution_quality": "high"
311
+ },
312
+ "sa": {
313
+ "name": "Simulated Annealing",
314
+ "description": "Probabilistic optimization inspired by metallurgy",
315
+ "typical_time": "medium",
316
+ "solution_quality": "medium-high"
317
+ },
318
+ "nsga2": {
319
+ "name": "NSGA-II",
320
+ "description": "Non-dominated Sorting Genetic Algorithm (multi-objective)",
321
+ "typical_time": "high",
322
+ "solution_quality": "very high"
323
+ },
324
+ "adaptive": {
325
+ "name": "Adaptive Optimizer",
326
+ "description": "Automatically selects best algorithm",
327
+ "typical_time": "high",
328
+ "solution_quality": "very high"
329
+ },
330
+ "ensemble": {
331
+ "name": "Ensemble Optimizer",
332
+ "description": "Runs multiple algorithms in parallel",
333
+ "typical_time": "high",
334
+ "solution_quality": "highest"
335
+ }
336
+ },
337
+ "default_method": "ga",
338
+ "recommended_for_speed": "ga",
339
+ "recommended_for_quality": "ensemble"
340
+ }
341
+
342
+
343
+ @app.post("/optimize", response_model=ScheduleOptimizationResponse)
344
+ async def optimize_schedule(request: ScheduleOptimizationRequest):
345
+ """
346
+ Optimize train schedule with custom input data.
347
+
348
+ This endpoint accepts detailed trainset data and returns an optimized schedule
349
+ that maximizes service coverage while respecting all constraints.
350
+ """
351
+ try:
352
+ import time
353
+ start_time = time.time()
354
+
355
+ logger.info(f"Received optimization request with {len(request.trainset_status)} trainsets, method: {request.method}")
356
+
357
+ # Convert request to dict format
358
+ data = convert_pydantic_to_dict(request)
359
+
360
+ # Validate data
361
+ validation_errors = DataValidator.validate_data(data)
362
+ if validation_errors:
363
+ raise HTTPException(
364
+ status_code=400,
365
+ detail={
366
+ "error": "Data validation failed",
367
+ "validation_errors": validation_errors,
368
+ "message": "Please fix the data structure and try again"
369
+ }
370
+ )
371
+
372
+ # Convert config
373
+ config = convert_config(request.config)
374
+
375
+ # Run optimization
376
+ result = optimize_trainset_schedule(data, request.method, config)
377
+
378
+ execution_time = time.time() - start_time
379
+
380
+ logger.info(f"Optimization completed in {execution_time:.3f}s, fitness: {result.fitness_score:.4f}")
381
+
382
+ # Convert to response
383
+ response = convert_result_to_response(result, request.method, execution_time)
384
+
385
+ return response
386
+
387
+ except HTTPException:
388
+ raise
389
+ except Exception as e:
390
+ logger.error(f"Optimization error: {str(e)}", exc_info=True)
391
+ raise HTTPException(
392
+ status_code=500,
393
+ detail={
394
+ "error": "Optimization failed",
395
+ "message": str(e),
396
+ "type": type(e).__name__
397
+ }
398
+ )
399
+
400
+
401
+ @app.post("/compare")
402
+ async def compare_methods(request: CompareMethodsRequest):
403
+ """
404
+ Compare multiple optimization methods on the same input data.
405
+
406
+ Returns results from all requested methods for comparison.
407
+ """
408
+ try:
409
+ import time
410
+
411
+ logger.info(f"Comparing methods: {request.methods}")
412
+
413
+ # Create a temporary request object for conversion
414
+ temp_request = ScheduleOptimizationRequest(
415
+ trainset_status=request.trainset_status,
416
+ fitness_certificates=request.fitness_certificates,
417
+ job_cards=request.job_cards,
418
+ component_health=request.component_health,
419
+ metadata=request.metadata,
420
+ date=request.date,
421
+ method="ga" # Default method for conversion
422
+ )
423
+
424
+ # Convert request to dict format
425
+ data = convert_pydantic_to_dict(temp_request)
426
+
427
+ # Validate data
428
+ validation_errors = DataValidator.validate_data(data)
429
+ if validation_errors:
430
+ raise HTTPException(status_code=400, detail={"error": "Data validation failed", "details": validation_errors})
431
+
432
+ # Convert config
433
+ config = convert_config(request.config)
434
+
435
+ # Compare methods
436
+ start_time = time.time()
437
+ results = compare_optimization_methods(data, request.methods, config)
438
+ total_time = time.time() - start_time
439
+
440
+ # Convert results
441
+ comparison = {
442
+ "methods": {},
443
+ "summary": {
444
+ "total_execution_time": total_time,
445
+ "methods_compared": len(results),
446
+ "timestamp": datetime.now().isoformat()
447
+ }
448
+ }
449
+
450
+ best_score = -float('inf')
451
+ best_method = None
452
+
453
+ for method, result in results.items():
454
+ comparison["methods"][method] = convert_result_to_response(
455
+ result, method
456
+ ).dict()
457
+
458
+ if result.fitness_score > best_score:
459
+ best_score = result.fitness_score
460
+ best_method = method
461
+
462
+ comparison["summary"]["best_method"] = best_method
463
+ comparison["summary"]["best_score"] = best_score
464
+
465
+ logger.info(f"Comparison completed, best: {best_method} ({best_score:.4f})")
466
+
467
+ return JSONResponse(content=comparison)
468
+
469
+ except Exception as e:
470
+ logger.error(f"Comparison error: {str(e)}", exc_info=True)
471
+ raise HTTPException(
472
+ status_code=500,
473
+ detail={"error": "Comparison failed", "message": str(e)}
474
+ )
475
+
476
+
477
+ @app.post("/generate-synthetic")
478
+ async def generate_synthetic_data(request: SyntheticDataRequest):
479
+ """
480
+ Generate synthetic test data using EnhancedMetroDataGenerator.
481
+
482
+ Useful for testing the optimization API without providing real data.
483
+ """
484
+ try:
485
+ logger.info(f"Generating synthetic data for {request.num_trainsets} trainsets")
486
+
487
+ # Generate data
488
+ generator = EnhancedMetroDataGenerator(num_trainsets=request.num_trainsets)
489
+ data = generator.generate_complete_enhanced_dataset()
490
+
491
+ # Filter to match request parameters
492
+ # (Optional: adjust availability based on request params)
493
+
494
+ logger.info(f"Generated synthetic data with {len(data['trainset_status'])} trainsets")
495
+
496
+ return JSONResponse(content={
497
+ "success": True,
498
+ "data": data,
499
+ "metadata": {
500
+ "num_trainsets": len(data['trainset_status']),
501
+ "num_fitness_certificates": len(data['fitness_certificates']),
502
+ "num_job_cards": len(data['job_cards']),
503
+ "num_component_health": len(data['component_health']),
504
+ "generated_at": datetime.now().isoformat()
505
+ }
506
+ })
507
+
508
+ except Exception as e:
509
+ logger.error(f"Synthetic data generation error: {str(e)}", exc_info=True)
510
+ raise HTTPException(
511
+ status_code=500,
512
+ detail={"error": "Data generation failed", "message": str(e)}
513
+ )
514
+
515
+
516
+ @app.post("/validate")
517
+ async def validate_data(request: ScheduleOptimizationRequest):
518
+ """
519
+ Validate input data structure without running optimization.
520
+
521
+ Returns validation results and suggestions for fixing issues.
522
+ """
523
+ try:
524
+ # Convert to dict
525
+ data = convert_pydantic_to_dict(request)
526
+
527
+ # Validate
528
+ validation_errors = DataValidator.validate_data(data)
529
+
530
+ if not validation_errors:
531
+ return {
532
+ "valid": True,
533
+ "message": "Data structure is valid",
534
+ "num_trainsets": len(request.trainset_status),
535
+ "num_certificates": len(request.fitness_certificates),
536
+ "num_job_cards": len(request.job_cards),
537
+ "num_component_health": len(request.component_health)
538
+ }
539
+
540
+ return {
541
+ "valid": False,
542
+ "validation_errors": validation_errors,
543
+ "suggestions": [
544
+ "Check that all trainset_ids are consistent across sections",
545
+ "Ensure operational_status values are valid (Available, In-Service, Maintenance, Standby, Out-of-Order)",
546
+ "Verify certificate expiry dates are in ISO format",
547
+ "Confirm component wear_level is between 0-100"
548
+ ]
549
+ }
550
+
551
+ except Exception as e:
552
+ raise HTTPException(
553
+ status_code=400,
554
+ detail={"error": "Validation failed", "message": str(e)}
555
+ )
556
+
557
+
558
+ if __name__ == "__main__":
559
+ import uvicorn
560
+ uvicorn.run(app, host="0.0.0.0", port=8001)
api/run_greedyoptim_api.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Startup script for GreedyOptim API
4
+ Run this to start the advanced optimization API service
5
+ """
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
+ import uvicorn
14
+
15
+ print("=" * 70)
16
+ print("GreedyOptim Scheduling API")
17
+ print("=" * 70)
18
+ print()
19
+ print("Starting FastAPI server on port 8001...")
20
+ print()
21
+ print("API Documentation: http://localhost:8001/docs")
22
+ print("Alternative Docs: http://localhost:8001/redoc")
23
+ print("Health Check: http://localhost:8001/health")
24
+ print("Available Methods: http://localhost:8001/methods")
25
+ print()
26
+ print("Main Endpoints:")
27
+ print(" POST /optimize - Optimize with custom data")
28
+ print(" POST /compare - Compare multiple methods")
29
+ print(" POST /generate-synthetic - Generate test data")
30
+ print(" POST /validate - Validate data structure")
31
+ print()
32
+ print("=" * 70)
33
+ print()
34
+
35
+ # Run the API
36
+ uvicorn.run(
37
+ "api.greedyoptim_api:app",
38
+ host="0.0.0.0",
39
+ port=8001,
40
+ reload=True,
41
+ log_level="info"
42
+ )
api/test_greedyoptim_api.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for GreedyOptim API
4
+ Tests all endpoints with sample data
5
+ """
6
+ import requests
7
+ import json
8
+ from datetime import datetime, timedelta
9
+
10
+ BASE_URL = "http://localhost:8001"
11
+
12
+
13
+ def test_health():
14
+ """Test health check endpoint"""
15
+ print("\n" + "="*70)
16
+ print("Testing Health Check")
17
+ print("="*70)
18
+
19
+ response = requests.get(f"{BASE_URL}/health")
20
+ print(f"Status: {response.status_code}")
21
+ print(f"Response: {json.dumps(response.json(), indent=2)}")
22
+ return response.status_code == 200
23
+
24
+
25
+ def test_methods():
26
+ """Test methods listing endpoint"""
27
+ print("\n" + "="*70)
28
+ print("Testing Methods Listing")
29
+ print("="*70)
30
+
31
+ response = requests.get(f"{BASE_URL}/methods")
32
+ print(f"Status: {response.status_code}")
33
+
34
+ if response.status_code == 200:
35
+ methods = response.json()
36
+ print(f"\nAvailable Methods: {len(methods['available_methods'])}")
37
+ for method, info in methods['available_methods'].items():
38
+ print(f" {method}: {info['name']}")
39
+
40
+ return response.status_code == 200
41
+
42
+
43
+ def test_generate_synthetic():
44
+ """Test synthetic data generation"""
45
+ print("\n" + "="*70)
46
+ print("Testing Synthetic Data Generation")
47
+ print("="*70)
48
+
49
+ payload = {
50
+ "num_trainsets": 20,
51
+ "maintenance_rate": 0.1,
52
+ "availability_rate": 0.8
53
+ }
54
+
55
+ response = requests.post(f"{BASE_URL}/generate-synthetic", json=payload)
56
+ print(f"Status: {response.status_code}")
57
+
58
+ if response.status_code == 200:
59
+ result = response.json()
60
+ print(f"\nGenerated Data:")
61
+ print(f" Trainsets: {result['metadata']['num_trainsets']}")
62
+ print(f" Fitness Certificates: {result['metadata']['num_fitness_certificates']}")
63
+ print(f" Job Cards: {result['metadata']['num_job_cards']}")
64
+ print(f" Component Health: {result['metadata']['num_component_health']}")
65
+ return result['data'] # Return for use in other tests
66
+
67
+ return None
68
+
69
+
70
+ def test_validate(data):
71
+ """Test data validation endpoint"""
72
+ print("\n" + "="*70)
73
+ print("Testing Data Validation")
74
+ print("="*70)
75
+
76
+ # Create request from synthetic data
77
+ request_data = {
78
+ "trainset_status": data['trainset_status'],
79
+ "fitness_certificates": data['fitness_certificates'],
80
+ "job_cards": data['job_cards'],
81
+ "component_health": data['component_health'],
82
+ "method": "ga"
83
+ }
84
+
85
+ response = requests.post(f"{BASE_URL}/validate", json=request_data)
86
+ print(f"Status: {response.status_code}")
87
+
88
+ if response.status_code == 200:
89
+ result = response.json()
90
+ print(f"\nValidation Result:")
91
+ print(f" Valid: {result['valid']}")
92
+ if result['valid']:
93
+ print(f" Trainsets: {result['num_trainsets']}")
94
+ print(f" Certificates: {result['num_certificates']}")
95
+ print(f" Job Cards: {result['num_job_cards']}")
96
+ print(f" Component Health: {result['num_component_health']}")
97
+ else:
98
+ print(f" Errors: {len(result.get('validation_errors', []))}")
99
+
100
+ return response.status_code == 200
101
+
102
+
103
+ def test_optimize(data):
104
+ """Test optimization endpoint"""
105
+ print("\n" + "="*70)
106
+ print("Testing Schedule Optimization")
107
+ print("="*70)
108
+
109
+ # Create optimization request
110
+ request_data = {
111
+ "trainset_status": data['trainset_status'],
112
+ "fitness_certificates": data['fitness_certificates'],
113
+ "job_cards": data['job_cards'],
114
+ "component_health": data['component_health'],
115
+ "method": "ga",
116
+ "config": {
117
+ "required_service_trains": 15,
118
+ "min_standby": 2,
119
+ "population_size": 30,
120
+ "generations": 50
121
+ }
122
+ }
123
+
124
+ print(f"Optimizing with method: {request_data['method']}")
125
+ print(f"Trainsets: {len(request_data['trainset_status'])}")
126
+
127
+ response = requests.post(f"{BASE_URL}/optimize", json=request_data)
128
+ print(f"Status: {response.status_code}")
129
+
130
+ if response.status_code == 200:
131
+ result = response.json()
132
+ print(f"\nOptimization Results:")
133
+ print(f" Method: {result['method']}")
134
+ print(f" Fitness Score: {result['fitness_score']:.4f}")
135
+ print(f" Execution Time: {result['execution_time_seconds']:.3f}s")
136
+ print(f"\n Schedule Allocation:")
137
+ print(f" In Service: {result['num_service']} trains")
138
+ print(f" Standby: {result['num_standby']} trains")
139
+ print(f" Maintenance: {result['num_maintenance']} trains")
140
+ print(f" Unavailable: {result['num_unavailable']} trains")
141
+ print(f"\n Detailed Scores:")
142
+ print(f" Service: {result['service_score']:.4f}")
143
+ print(f" Standby: {result['standby_score']:.4f}")
144
+ print(f" Health: {result['health_score']:.4f}")
145
+ print(f" Certificate: {result['certificate_score']:.4f}")
146
+ print(f"\n Constraints Satisfied: {result['constraints_satisfied']}")
147
+
148
+ if result.get('warnings'):
149
+ print(f" Warnings: {len(result['warnings'])}")
150
+ for warning in result['warnings'][:3]:
151
+ print(f" - {warning}")
152
+ else:
153
+ print(f"Error: {response.text}")
154
+
155
+ return response.status_code == 200
156
+
157
+
158
+ def test_compare(data):
159
+ """Test method comparison endpoint"""
160
+ print("\n" + "="*70)
161
+ print("Testing Method Comparison")
162
+ print("="*70)
163
+
164
+ # Create comparison request
165
+ request_data = {
166
+ "trainset_status": data['trainset_status'][:15], # Use smaller dataset for faster comparison
167
+ "fitness_certificates": [fc for fc in data['fitness_certificates'] if fc['trainset_id'] in [ts['trainset_id'] for ts in data['trainset_status'][:15]]],
168
+ "job_cards": [jc for jc in data['job_cards'] if jc['trainset_id'] in [ts['trainset_id'] for ts in data['trainset_status'][:15]]],
169
+ "component_health": [ch for ch in data['component_health'] if ch['trainset_id'] in [ts['trainset_id'] for ts in data['trainset_status'][:15]]],
170
+ "methods": ["ga", "pso"],
171
+ "config": {
172
+ "population_size": 20,
173
+ "generations": 30
174
+ }
175
+ }
176
+
177
+ print(f"Comparing methods: {request_data['methods']}")
178
+ print(f"Trainsets: {len(request_data['trainset_status'])}")
179
+
180
+ response = requests.post(f"{BASE_URL}/compare", json=request_data)
181
+ print(f"Status: {response.status_code}")
182
+
183
+ if response.status_code == 200:
184
+ result = response.json()
185
+ print(f"\nComparison Results:")
186
+ print(f" Total Execution Time: {result['summary']['total_execution_time']:.3f}s")
187
+ print(f" Best Method: {result['summary']['best_method']}")
188
+ print(f" Best Score: {result['summary']['best_score']:.4f}")
189
+
190
+ print(f"\n Individual Results:")
191
+ for method, method_result in result['methods'].items():
192
+ print(f" {method.upper()}:")
193
+ print(f" Fitness: {method_result['fitness_score']:.4f}")
194
+ print(f" Service: {method_result['num_service']} trains")
195
+ print(f" Time: {method_result.get('execution_time_seconds', 'N/A')}")
196
+ else:
197
+ print(f"Error: {response.text}")
198
+
199
+ return response.status_code == 200
200
+
201
+
202
+ def test_custom_data():
203
+ """Test with minimal custom data"""
204
+ print("\n" + "="*70)
205
+ print("Testing with Custom Minimal Data")
206
+ print("="*70)
207
+
208
+ # Create minimal valid data
209
+ custom_data = {
210
+ "trainset_status": [
211
+ {"trainset_id": f"KMRL-{i:02d}", "operational_status": "Available"}
212
+ for i in range(1, 11)
213
+ ],
214
+ "fitness_certificates": [
215
+ {
216
+ "trainset_id": f"KMRL-{i:02d}",
217
+ "department": "Safety",
218
+ "status": "Valid",
219
+ "expiry_date": (datetime.now() + timedelta(days=365)).isoformat()
220
+ }
221
+ for i in range(1, 11)
222
+ ],
223
+ "job_cards": [], # No job cards
224
+ "component_health": [
225
+ {
226
+ "trainset_id": f"KMRL-{i:02d}",
227
+ "component": "Brakes",
228
+ "status": "Good",
229
+ "wear_level": 20.0
230
+ }
231
+ for i in range(1, 11)
232
+ ],
233
+ "method": "ga",
234
+ "config": {
235
+ "required_service_trains": 8,
236
+ "min_standby": 1,
237
+ "population_size": 20,
238
+ "generations": 30
239
+ }
240
+ }
241
+
242
+ print(f"Testing with {len(custom_data['trainset_status'])} trainsets")
243
+
244
+ response = requests.post(f"{BASE_URL}/optimize", json=custom_data)
245
+ print(f"Status: {response.status_code}")
246
+
247
+ if response.status_code == 200:
248
+ result = response.json()
249
+ print(f"\nOptimization successful!")
250
+ print(f" Fitness: {result['fitness_score']:.4f}")
251
+ print(f" In Service: {result['num_service']}")
252
+ print(f" Time: {result['execution_time_seconds']:.3f}s")
253
+ else:
254
+ print(f"Error: {response.text}")
255
+
256
+ return response.status_code == 200
257
+
258
+
259
+ def main():
260
+ """Run all tests"""
261
+ print("=" * 70)
262
+ print("GREEDYOPTIM API TEST SUITE")
263
+ print("=" * 70)
264
+ print(f"Testing API at: {BASE_URL}")
265
+ print(f"Start Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
266
+
267
+ results = {}
268
+
269
+ # Run tests
270
+ results['health'] = test_health()
271
+ results['methods'] = test_methods()
272
+
273
+ # Generate synthetic data for remaining tests
274
+ synthetic_data = test_generate_synthetic()
275
+
276
+ if synthetic_data:
277
+ results['validate'] = test_validate(synthetic_data)
278
+ results['optimize'] = test_optimize(synthetic_data)
279
+ results['compare'] = test_compare(synthetic_data)
280
+
281
+ results['custom'] = test_custom_data()
282
+
283
+ # Summary
284
+ print("\n" + "="*70)
285
+ print("TEST SUMMARY")
286
+ print("="*70)
287
+
288
+ passed = sum(1 for v in results.values() if v)
289
+ total = len(results)
290
+
291
+ print(f"\nTests Passed: {passed}/{total}")
292
+ for test_name, passed in results.items():
293
+ status = "✓ PASS" if passed else "✗ FAIL"
294
+ print(f" {status} - {test_name}")
295
+
296
+ print("\n" + "="*70)
297
+ print(f"End Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
298
+ print("="*70)
299
+
300
+
301
+ if __name__ == "__main__":
302
+ try:
303
+ main()
304
+ except requests.exceptions.ConnectionError:
305
+ print("\n✗ ERROR: Could not connect to API")
306
+ print(" Make sure the API is running:")
307
+ print(" python api/run_greedyoptim_api.py")
308
+ except Exception as e:
309
+ print(f"\n✗ ERROR: {str(e)}")