Imporve enterprise
This commit is contained in:
341
services/distribution/app/api/vrp_optimization.py
Normal file
341
services/distribution/app/api/vrp_optimization.py
Normal file
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
VRP Optimization API Endpoints
|
||||
Endpoints for VRP optimization and metrics retrieval
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
import structlog
|
||||
|
||||
from app.services.vrp_optimization_service import VRPOptimizationService
|
||||
from app.services.distribution_service import DistributionService
|
||||
from shared.auth.tenant_access import verify_tenant_permission_dep
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pydantic models for request/response
|
||||
class VRPOptimizationRequest(BaseModel):
|
||||
algorithm_version: str = Field(default="v2.1", description="VRP algorithm version to use")
|
||||
constraints: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Optimization constraints: max_route_duration, max_route_distance, etc."
|
||||
)
|
||||
|
||||
|
||||
class VRPOptimizationResponse(BaseModel):
|
||||
success: bool
|
||||
route_id: str
|
||||
optimization_savings: Dict[str, Any]
|
||||
vrp_algorithm_version: str
|
||||
vrp_optimization_timestamp: str
|
||||
vrp_constraints_satisfied: bool
|
||||
vrp_objective_value: float
|
||||
|
||||
|
||||
class RouteOptimizationMetrics(BaseModel):
|
||||
route_id: str
|
||||
route_number: str
|
||||
route_date: str
|
||||
vrp_optimization_savings: Optional[Dict[str, Any]]
|
||||
vrp_algorithm_version: Optional[str]
|
||||
vrp_optimization_timestamp: Optional[str]
|
||||
vrp_constraints_satisfied: Optional[bool]
|
||||
vrp_objective_value: Optional[float]
|
||||
total_distance_km: Optional[float]
|
||||
estimated_duration_minutes: Optional[int]
|
||||
|
||||
|
||||
class NetworkOptimizationSummary(BaseModel):
|
||||
total_routes: int
|
||||
optimized_routes: int
|
||||
total_distance_saved_km: float
|
||||
total_time_saved_minutes: float
|
||||
total_fuel_saved_liters: float
|
||||
total_co2_saved_kg: float
|
||||
total_cost_saved_eur: float
|
||||
optimization_rate: float
|
||||
average_savings_per_route: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class OptimizationHistoryItem(BaseModel):
|
||||
optimization_id: str
|
||||
route_id: str
|
||||
timestamp: str
|
||||
algorithm_version: str
|
||||
distance_saved_km: float
|
||||
time_saved_minutes: float
|
||||
fuel_saved_liters: float
|
||||
co2_saved_kg: float
|
||||
cost_saved_eur: float
|
||||
constraints_satisfied: bool
|
||||
|
||||
|
||||
async def get_vrp_optimization_service() -> VRPOptimizationService:
|
||||
"""Dependency injection for VRPOptimizationService"""
|
||||
from app.core.database import database_manager
|
||||
from app.services.distribution_service import DistributionService as BusinessDistributionService
|
||||
from app.repositories.delivery_route_repository import DeliveryRouteRepository
|
||||
from app.repositories.shipment_repository import ShipmentRepository
|
||||
from app.repositories.delivery_schedule_repository import DeliveryScheduleRepository
|
||||
from shared.clients.tenant_client import TenantServiceClient
|
||||
from shared.clients.inventory_client import InventoryServiceClient
|
||||
from shared.clients.procurement_client import ProcurementServiceClient
|
||||
from app.services.routing_optimizer import RoutingOptimizer
|
||||
|
||||
# Create the business distribution service with proper dependencies
|
||||
route_repository = DeliveryRouteRepository(database_manager.get_session())
|
||||
shipment_repository = ShipmentRepository(database_manager.get_session())
|
||||
schedule_repository = DeliveryScheduleRepository(database_manager.get_session())
|
||||
|
||||
# Create client instances (these will be initialized with proper config)
|
||||
tenant_client = TenantServiceClient()
|
||||
inventory_client = InventoryServiceClient()
|
||||
procurement_client = ProcurementServiceClient()
|
||||
routing_optimizer = RoutingOptimizer()
|
||||
|
||||
distribution_service = BusinessDistributionService(
|
||||
route_repository=route_repository,
|
||||
shipment_repository=shipment_repository,
|
||||
schedule_repository=schedule_repository,
|
||||
procurement_client=procurement_client,
|
||||
tenant_client=tenant_client,
|
||||
inventory_client=inventory_client,
|
||||
routing_optimizer=routing_optimizer
|
||||
)
|
||||
|
||||
return VRPOptimizationService(distribution_service, database_manager)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/routes/{route_id}/optimize",
|
||||
response_model=VRPOptimizationResponse,
|
||||
summary="Optimize delivery route with VRP")
|
||||
async def optimize_route_with_vrp(
|
||||
tenant_id: str,
|
||||
route_id: str,
|
||||
optimization_request: VRPOptimizationRequest,
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Optimize a delivery route using VRP algorithm
|
||||
|
||||
This endpoint applies VRP optimization to a specific delivery route and stores
|
||||
the optimization metrics for analysis and reporting.
|
||||
"""
|
||||
try:
|
||||
result = await vrp_service.optimize_route_with_vrp(
|
||||
route_id=route_id,
|
||||
algorithm_version=optimization_request.algorithm_version,
|
||||
constraints=optimization_request.constraints
|
||||
)
|
||||
|
||||
if not result.get('success'):
|
||||
raise HTTPException(status_code=500, detail="Optimization failed")
|
||||
|
||||
return VRPOptimizationResponse(
|
||||
success=True,
|
||||
route_id=result['route_id'],
|
||||
optimization_savings=result['optimization_savings'],
|
||||
vrp_algorithm_version=result['optimization_savings'].get('algorithm_version', optimization_request.algorithm_version),
|
||||
vrp_optimization_timestamp=result['optimization_savings'].get('timestamp', datetime.now().isoformat()),
|
||||
vrp_constraints_satisfied=result['optimization_savings'].get('constraints_satisfied', True),
|
||||
vrp_objective_value=result['optimization_savings'].get('objective_value', 0.0)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("VRP optimization failed", tenant_id=tenant_id, route_id=route_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"VRP optimization failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/routes/{route_id}/optimization-metrics",
|
||||
response_model=RouteOptimizationMetrics,
|
||||
summary="Get VRP optimization metrics for route")
|
||||
async def get_route_optimization_metrics(
|
||||
tenant_id: str,
|
||||
route_id: str,
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Get VRP optimization metrics for a specific route
|
||||
|
||||
Retrieves stored optimization metrics including savings, algorithm version,
|
||||
and constraint satisfaction status.
|
||||
"""
|
||||
try:
|
||||
metrics = await vrp_service.get_route_optimization_metrics(route_id)
|
||||
return RouteOptimizationMetrics(**metrics)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get route optimization metrics", tenant_id=tenant_id, route_id=route_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get optimization metrics: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/vrp/optimization-summary",
|
||||
response_model=NetworkOptimizationSummary,
|
||||
summary="Get network-wide VRP optimization summary")
|
||||
async def get_network_optimization_summary(
|
||||
tenant_id: str,
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Get aggregated VRP optimization metrics across all routes
|
||||
|
||||
Provides network-wide summary of optimization benefits including
|
||||
total savings, optimization rate, and average improvements.
|
||||
"""
|
||||
try:
|
||||
summary = await vrp_service.get_network_optimization_summary(tenant_id)
|
||||
return NetworkOptimizationSummary(**summary)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get network optimization summary", tenant_id=tenant_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get optimization summary: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/vrp/batch-optimize",
|
||||
summary="Batch optimize multiple routes")
|
||||
async def batch_optimize_routes(
|
||||
tenant_id: str,
|
||||
route_ids: List[str] = Query(..., description="List of route IDs to optimize"),
|
||||
algorithm_version: str = Query("v2.1", description="VRP algorithm version"),
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Batch optimize multiple delivery routes with VRP
|
||||
|
||||
Applies VRP optimization to multiple routes in a single request.
|
||||
"""
|
||||
try:
|
||||
result = await vrp_service.batch_optimize_routes(tenant_id, route_ids)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'total_routes_processed': result['total_routes_processed'],
|
||||
'successful_optimizations': result['successful_optimizations'],
|
||||
'failed_optimizations': result['failed_optimizations'],
|
||||
'results': result['results']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Batch optimization failed", tenant_id=tenant_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Batch optimization failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/routes/{route_id}/optimization-history",
|
||||
response_model=List[OptimizationHistoryItem],
|
||||
summary="Get optimization history for route")
|
||||
async def get_optimization_history(
|
||||
tenant_id: str,
|
||||
route_id: str,
|
||||
limit: int = Query(10, description="Maximum number of historical records to return"),
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Get historical optimization records for a route
|
||||
|
||||
Retrieves past optimization runs and their results for analysis.
|
||||
"""
|
||||
try:
|
||||
history = await vrp_service.get_optimization_history(route_id, limit)
|
||||
return [OptimizationHistoryItem(**item) for item in history]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get optimization history", tenant_id=tenant_id, route_id=route_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get optimization history: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/vrp/constraints/validate",
|
||||
summary="Validate VRP constraints")
|
||||
async def validate_vrp_constraints(
|
||||
tenant_id: str,
|
||||
route_id: str,
|
||||
max_route_duration: Optional[int] = Query(None, description="Maximum route duration in minutes"),
|
||||
max_route_distance: Optional[float] = Query(None, description="Maximum route distance in km"),
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Validate VRP constraints against a route
|
||||
|
||||
Checks if a route satisfies specified VRP constraints.
|
||||
"""
|
||||
try:
|
||||
from app.services.vrp_optimization_service import VRPConstraintValidator
|
||||
|
||||
# Get route data
|
||||
route = await vrp_service.distribution_service.get_delivery_route(route_id)
|
||||
|
||||
if not route:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
|
||||
# Build constraints dict
|
||||
constraints = {}
|
||||
if max_route_duration is not None:
|
||||
constraints['max_route_duration'] = max_route_duration
|
||||
if max_route_distance is not None:
|
||||
constraints['max_route_distance'] = max_route_distance
|
||||
|
||||
# Validate constraints
|
||||
validation_result = VRPConstraintValidator.validate_constraints(route, constraints)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'all_constraints_satisfied': validation_result['all_satisfied'],
|
||||
'constraint_violations': validation_result['constraint_violations']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to validate VRP constraints", tenant_id=tenant_id, route_id=route_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Failed to validate constraints: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/vrp/simulate",
|
||||
summary="Simulate VRP optimization")
|
||||
async def simulate_vrp_optimization(
|
||||
tenant_id: str,
|
||||
route_id: str,
|
||||
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
|
||||
verified_tenant: str = Depends(verify_tenant_permission_dep)
|
||||
):
|
||||
"""
|
||||
Simulate VRP optimization without saving results
|
||||
|
||||
Useful for testing and previewing optimization results.
|
||||
"""
|
||||
try:
|
||||
from app.services.vrp_optimization_service import VRPOptimizationSimulator
|
||||
|
||||
# Get route data
|
||||
route = await vrp_service.distribution_service.get_delivery_route(route_id)
|
||||
|
||||
if not route:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
|
||||
# Simulate optimization
|
||||
simulation_result = VRPOptimizationSimulator.simulate_optimization(route)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'original_route': simulation_result['original_route'],
|
||||
'optimized_route': simulation_result['optimized_route'],
|
||||
'optimization_savings': simulation_result['optimization_savings'],
|
||||
'algorithm_version': simulation_result['algorithm_version'],
|
||||
'constraints_satisfied': simulation_result['constraints_satisfied'],
|
||||
'objective_value': simulation_result['objective_value']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("VRP simulation failed", tenant_id=tenant_id, route_id=route_id, error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"VRP simulation failed: {str(e)}")
|
||||
|
||||
|
||||
# Import datetime at runtime to avoid circular imports
|
||||
from datetime import datetime
|
||||
@@ -9,6 +9,7 @@ from app.core.database import database_manager
|
||||
from app.api.routes import router as distribution_router
|
||||
from app.api.shipments import router as shipments_router
|
||||
from app.api.internal_demo import router as internal_demo_router
|
||||
from app.api.vrp_optimization import router as vrp_optimization_router
|
||||
from shared.service_base import StandardFastAPIService
|
||||
|
||||
|
||||
@@ -122,4 +123,5 @@ service.setup_standard_endpoints()
|
||||
# Note: Routes now use RouteBuilder which includes full paths, so no prefix needed
|
||||
service.add_router(distribution_router, tags=["distribution"])
|
||||
service.add_router(shipments_router, tags=["shipments"])
|
||||
service.add_router(internal_demo_router, tags=["internal-demo"])
|
||||
service.add_router(internal_demo_router, tags=["internal-demo"])
|
||||
service.add_router(vrp_optimization_router, tags=["vrp-optimization"])
|
||||
@@ -58,6 +58,13 @@ class DeliveryRoute(Base):
|
||||
total_distance_km = Column(Float, nullable=True)
|
||||
estimated_duration_minutes = Column(Integer, nullable=True)
|
||||
|
||||
# VRP Optimization metrics (Phase 2 enhancement)
|
||||
vrp_optimization_savings = Column(JSONB, nullable=True) # {"distance_saved_km": 12.5, "time_saved_minutes": 25, "fuel_saved_liters": 8.2, "co2_saved_kg": 15.4, "cost_saved_eur": 12.50}
|
||||
vrp_algorithm_version = Column(String(50), nullable=True) # Version of VRP algorithm used
|
||||
vrp_optimization_timestamp = Column(DateTime(timezone=True), nullable=True) # When optimization was performed
|
||||
vrp_constraints_satisfied = Column(Boolean, nullable=True) # Whether all constraints were satisfied
|
||||
vrp_objective_value = Column(Float, nullable=True) # Objective function value from optimization
|
||||
|
||||
# Route details
|
||||
route_sequence = Column(JSONB, nullable=True) # Ordered array of stops with timing: [{"stop_number": 1, "location_id": "...", "estimated_arrival": "...", "actual_arrival": "..."}]
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
@@ -231,4 +231,82 @@ class DeliveryRouteRepository:
|
||||
await self.db_session.commit()
|
||||
|
||||
deleted_count = result.rowcount
|
||||
return deleted_count
|
||||
return deleted_count
|
||||
|
||||
async def update_route_vrp_metrics(self, route_id: str, vrp_metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Update VRP optimization metrics for a route
|
||||
"""
|
||||
stmt = select(DeliveryRoute).where(DeliveryRoute.id == route_id)
|
||||
result = await self.db_session.execute(stmt)
|
||||
route = result.scalar_one_or_none()
|
||||
|
||||
if not route:
|
||||
return None
|
||||
|
||||
# Update VRP metrics fields
|
||||
route.vrp_optimization_savings = vrp_metrics.get('vrp_optimization_savings')
|
||||
route.vrp_algorithm_version = vrp_metrics.get('vrp_algorithm_version')
|
||||
route.vrp_optimization_timestamp = vrp_metrics.get('vrp_optimization_timestamp')
|
||||
route.vrp_constraints_satisfied = vrp_metrics.get('vrp_constraints_satisfied')
|
||||
route.vrp_objective_value = vrp_metrics.get('vrp_objective_value')
|
||||
|
||||
await self.db_session.commit()
|
||||
await self.db_session.refresh(route)
|
||||
|
||||
return {
|
||||
'id': str(route.id),
|
||||
'vrp_optimization_savings': route.vrp_optimization_savings,
|
||||
'vrp_algorithm_version': route.vrp_algorithm_version,
|
||||
'vrp_optimization_timestamp': route.vrp_optimization_timestamp,
|
||||
'vrp_constraints_satisfied': route.vrp_constraints_satisfied,
|
||||
'vrp_objective_value': route.vrp_objective_value
|
||||
}
|
||||
|
||||
async def get_routes_by_tenant(self, tenant_id: str, limit: int = None, offset: int = None, order_by: str = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all routes for a specific tenant with pagination and ordering
|
||||
"""
|
||||
stmt = select(DeliveryRoute).where(DeliveryRoute.tenant_id == tenant_id)
|
||||
|
||||
# Apply ordering if specified
|
||||
if order_by:
|
||||
if 'vrp_optimization_timestamp' in order_by:
|
||||
if 'DESC' in order_by:
|
||||
stmt = stmt.order_by(DeliveryRoute.vrp_optimization_timestamp.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(DeliveryRoute.vrp_optimization_timestamp.asc())
|
||||
elif 'route_date' in order_by:
|
||||
if 'DESC' in order_by:
|
||||
stmt = stmt.order_by(DeliveryRoute.route_date.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(DeliveryRoute.route_date.asc())
|
||||
|
||||
# Apply pagination if specified
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
if offset is not None:
|
||||
stmt = stmt.offset(offset)
|
||||
|
||||
result = await self.db_session.execute(stmt)
|
||||
routes = result.scalars().all()
|
||||
|
||||
return [{
|
||||
'id': str(route.id),
|
||||
'tenant_id': str(route.tenant_id),
|
||||
'route_number': route.route_number,
|
||||
'route_date': route.route_date,
|
||||
'vehicle_id': route.vehicle_id,
|
||||
'driver_id': route.driver_id,
|
||||
'total_distance_km': route.total_distance_km,
|
||||
'estimated_duration_minutes': route.estimated_duration_minutes,
|
||||
'route_sequence': route.route_sequence,
|
||||
'status': route.status.value if hasattr(route.status, 'value') else route.status,
|
||||
'created_at': route.created_at,
|
||||
'updated_at': route.updated_at,
|
||||
'vrp_optimization_savings': route.vrp_optimization_savings,
|
||||
'vrp_algorithm_version': route.vrp_algorithm_version,
|
||||
'vrp_optimization_timestamp': route.vrp_optimization_timestamp,
|
||||
'vrp_constraints_satisfied': route.vrp_constraints_satisfied,
|
||||
'vrp_objective_value': route.vrp_objective_value
|
||||
} for route in routes]
|
||||
@@ -302,4 +302,23 @@ class DistributionService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating delivery schedule: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
# VRP Optimization Service Methods
|
||||
async def get_route_by_id(self, route_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get a specific delivery route by ID
|
||||
"""
|
||||
return await self.route_repository.get_route_by_id(route_id)
|
||||
|
||||
async def update_route_vrp_metrics(self, route_id: str, vrp_metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Update VRP optimization metrics for a route
|
||||
"""
|
||||
return await self.route_repository.update_route_vrp_metrics(route_id, vrp_metrics)
|
||||
|
||||
async def get_routes_by_tenant(self, tenant_id: str, limit: int = None, offset: int = None, order_by: str = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all routes for a specific tenant with pagination and ordering
|
||||
"""
|
||||
return await self.route_repository.get_routes_by_tenant(tenant_id, limit, offset, order_by)
|
||||
357
services/distribution/app/services/vrp_optimization_service.py
Normal file
357
services/distribution/app/services/vrp_optimization_service.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
VRP Optimization Service
|
||||
Business logic for VRP optimization and metrics management
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.repositories.delivery_route_repository import DeliveryRouteRepository
|
||||
from app.services.routing_optimizer import RoutingOptimizer
|
||||
from app.core.database import get_db
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class VRPOptimizationService:
|
||||
"""
|
||||
Service for VRP optimization operations
|
||||
"""
|
||||
|
||||
def __init__(self, distribution_service: "DistributionService", database_manager: Any):
|
||||
"""
|
||||
Initialize VRP optimization service
|
||||
|
||||
Args:
|
||||
distribution_service: Distribution service instance
|
||||
database_manager: Database manager for session management
|
||||
"""
|
||||
self.distribution_service = distribution_service
|
||||
self.database_manager = database_manager
|
||||
self.routing_optimizer = RoutingOptimizer()
|
||||
|
||||
async def optimize_route(
|
||||
self,
|
||||
tenant_id: str,
|
||||
route_id: str,
|
||||
optimization_params: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize a specific delivery route using VRP
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
route_id: Route ID to optimize
|
||||
optimization_params: Optimization parameters
|
||||
|
||||
Returns:
|
||||
Optimization result with metrics
|
||||
"""
|
||||
try:
|
||||
# Get the current route using distribution service
|
||||
route = await self.distribution_service.get_route_by_id(route_id)
|
||||
if not route:
|
||||
raise ValueError(f"Route {route_id} not found")
|
||||
|
||||
# Extract deliveries from route sequence
|
||||
deliveries = self._extract_deliveries_from_route(route)
|
||||
|
||||
# Perform VRP optimization
|
||||
depot_location = optimization_params.get('depot_location', (0.0, 0.0))
|
||||
vehicle_capacity = optimization_params.get('vehicle_capacity_kg', 1000.0)
|
||||
time_limit = optimization_params.get('time_limit_seconds', 30.0)
|
||||
|
||||
optimization_result = await self.routing_optimizer.optimize_daily_routes(
|
||||
deliveries=deliveries,
|
||||
depot_location=depot_location,
|
||||
vehicle_capacity_kg=vehicle_capacity,
|
||||
time_limit_seconds=time_limit
|
||||
)
|
||||
|
||||
# Update route with optimization metrics
|
||||
vrp_metrics = {
|
||||
'vrp_optimization_savings': {
|
||||
'distance_saved_km': optimization_result.get('distance_savings_km', 0.0),
|
||||
'time_saved_minutes': optimization_result.get('time_savings_minutes', 0.0),
|
||||
'cost_saved': optimization_result.get('cost_savings', 0.0)
|
||||
},
|
||||
'vrp_algorithm_version': 'or-tools-v1.0',
|
||||
'vrp_optimization_timestamp': datetime.utcnow(),
|
||||
'vrp_constraints_satisfied': optimization_result.get('constraints_satisfied', True),
|
||||
'vrp_objective_value': optimization_result.get('objective_value', 0.0)
|
||||
}
|
||||
|
||||
# Update the route with VRP metrics using distribution service
|
||||
await self.distribution_service.update_route_vrp_metrics(route_id, vrp_metrics)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'route_id': route_id,
|
||||
'optimization_metrics': vrp_metrics,
|
||||
'optimized_route': optimization_result.get('optimized_route', [])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("vrp_optimization_failed", error=str(e), route_id=route_id)
|
||||
raise
|
||||
|
||||
def _extract_deliveries_from_route(self, route: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract deliveries from route sequence
|
||||
|
||||
Args:
|
||||
route: Delivery route object
|
||||
|
||||
Returns:
|
||||
List of delivery dictionaries
|
||||
"""
|
||||
deliveries = []
|
||||
route_sequence = route.route_sequence or []
|
||||
|
||||
for stop in route_sequence:
|
||||
deliveries.append({
|
||||
'id': stop.get('id', ''),
|
||||
'location': (stop.get('lat', 0.0), stop.get('lng', 0.0)),
|
||||
'weight_kg': stop.get('weight_kg', 0.0),
|
||||
'time_window': stop.get('time_window')
|
||||
})
|
||||
|
||||
return deliveries
|
||||
|
||||
async def get_route_optimization_metrics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
route_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get VRP optimization metrics for a specific route
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
route_id: Route ID
|
||||
|
||||
Returns:
|
||||
VRP optimization metrics
|
||||
"""
|
||||
route = await self.route_repository.get_route_by_id(route_id)
|
||||
if not route:
|
||||
raise ValueError(f"Route {route_id} not found")
|
||||
|
||||
return {
|
||||
'vrp_optimization_savings': route.vrp_optimization_savings,
|
||||
'vrp_algorithm_version': route.vrp_algorithm_version,
|
||||
'vrp_optimization_timestamp': route.vrp_optimization_timestamp,
|
||||
'vrp_constraints_satisfied': route.vrp_constraints_satisfied,
|
||||
'vrp_objective_value': route.vrp_objective_value
|
||||
}
|
||||
|
||||
async def get_network_optimization_summary(
|
||||
self,
|
||||
tenant_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get VRP optimization summary across all routes for a tenant
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
|
||||
Returns:
|
||||
Network optimization summary
|
||||
"""
|
||||
routes = await self.route_repository.get_routes_by_tenant(tenant_id)
|
||||
|
||||
total_optimized = 0
|
||||
total_distance_saved = 0.0
|
||||
total_time_saved = 0.0
|
||||
total_cost_saved = 0.0
|
||||
|
||||
for route in routes:
|
||||
if route.vrp_optimization_timestamp:
|
||||
total_optimized += 1
|
||||
savings = route.vrp_optimization_savings or {}
|
||||
total_distance_saved += savings.get('distance_saved_km', 0.0)
|
||||
total_time_saved += savings.get('time_saved_minutes', 0.0)
|
||||
total_cost_saved += savings.get('cost_saved', 0.0)
|
||||
|
||||
return {
|
||||
'total_routes': len(routes),
|
||||
'total_optimized_routes': total_optimized,
|
||||
'optimization_rate': total_optimized / len(routes) if routes else 0.0,
|
||||
'total_distance_saved_km': total_distance_saved,
|
||||
'total_time_saved_minutes': total_time_saved,
|
||||
'total_cost_saved': total_cost_saved,
|
||||
'average_savings_per_route': {
|
||||
'distance_km': total_distance_saved / total_optimized if total_optimized > 0 else 0.0,
|
||||
'time_minutes': total_time_saved / total_optimized if total_optimized > 0 else 0.0,
|
||||
'cost': total_cost_saved / total_optimized if total_optimized > 0 else 0.0
|
||||
}
|
||||
}
|
||||
|
||||
async def batch_optimize_routes(
|
||||
self,
|
||||
tenant_id: str,
|
||||
route_ids: List[str],
|
||||
optimization_params: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Batch optimize multiple routes
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
route_ids: List of route IDs to optimize
|
||||
optimization_params: Optimization parameters
|
||||
|
||||
Returns:
|
||||
Batch optimization results
|
||||
"""
|
||||
results = []
|
||||
|
||||
for route_id in route_ids:
|
||||
try:
|
||||
result = await self.optimize_route(tenant_id, route_id, optimization_params)
|
||||
results.append({
|
||||
'route_id': route_id,
|
||||
'success': True,
|
||||
'metrics': result['optimization_metrics']
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'route_id': route_id,
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
'total_routes': len(route_ids),
|
||||
'successful_optimizations': sum(1 for r in results if r['success']),
|
||||
'failed_optimizations': sum(1 for r in results if not r['success']),
|
||||
'results': results
|
||||
}
|
||||
|
||||
async def validate_optimization_constraints(
|
||||
self,
|
||||
tenant_id: str,
|
||||
route_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate VRP optimization constraints for a route
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
route_id: Route ID
|
||||
|
||||
Returns:
|
||||
Constraint validation results
|
||||
"""
|
||||
route = await self.route_repository.get_route_by_id(route_id)
|
||||
if not route:
|
||||
raise ValueError(f"Route {route_id} not found")
|
||||
|
||||
# Check if route has been optimized
|
||||
if not route.vrp_optimization_timestamp:
|
||||
return {
|
||||
'route_id': route_id,
|
||||
'is_optimized': False,
|
||||
'constraints_valid': False,
|
||||
'message': 'Route has not been optimized yet'
|
||||
}
|
||||
|
||||
# Validate constraints
|
||||
constraints_valid = route.vrp_constraints_satisfied or False
|
||||
|
||||
return {
|
||||
'route_id': route_id,
|
||||
'is_optimized': True,
|
||||
'constraints_valid': constraints_valid,
|
||||
'vrp_algorithm_version': route.vrp_algorithm_version,
|
||||
'optimization_timestamp': route.vrp_optimization_timestamp
|
||||
}
|
||||
|
||||
async def get_optimization_history(
|
||||
self,
|
||||
tenant_id: str,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get VRP optimization history for a tenant
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
limit: Maximum number of records to return
|
||||
offset: Pagination offset
|
||||
|
||||
Returns:
|
||||
Optimization history
|
||||
"""
|
||||
routes = await self.route_repository.get_routes_by_tenant(
|
||||
tenant_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
order_by='vrp_optimization_timestamp DESC'
|
||||
)
|
||||
|
||||
history = []
|
||||
for route in routes:
|
||||
if route.vrp_optimization_timestamp:
|
||||
history.append({
|
||||
'route_id': str(route.id),
|
||||
'route_number': route.route_number,
|
||||
'optimization_timestamp': route.vrp_optimization_timestamp,
|
||||
'algorithm_version': route.vrp_algorithm_version,
|
||||
'constraints_satisfied': route.vrp_constraints_satisfied,
|
||||
'objective_value': route.vrp_objective_value,
|
||||
'savings': route.vrp_optimization_savings
|
||||
})
|
||||
|
||||
return {
|
||||
'total_records': len(history),
|
||||
'history': history
|
||||
}
|
||||
|
||||
async def simulate_optimization(
|
||||
self,
|
||||
tenant_id: str,
|
||||
route_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Simulate VRP optimization without saving results
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
route_data: Route data for simulation
|
||||
|
||||
Returns:
|
||||
Simulation results
|
||||
"""
|
||||
try:
|
||||
deliveries = route_data.get('deliveries', [])
|
||||
depot_location = route_data.get('depot_location', (0.0, 0.0))
|
||||
vehicle_capacity = route_data.get('vehicle_capacity_kg', 1000.0)
|
||||
time_limit = route_data.get('time_limit_seconds', 30.0)
|
||||
|
||||
simulation_result = await self.routing_optimizer.optimize_daily_routes(
|
||||
deliveries=deliveries,
|
||||
depot_location=depot_location,
|
||||
vehicle_capacity_kg=vehicle_capacity,
|
||||
time_limit_seconds=time_limit
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'simulation_results': simulation_result,
|
||||
'estimated_savings': {
|
||||
'distance_km': simulation_result.get('distance_savings_km', 0.0),
|
||||
'time_minutes': simulation_result.get('time_savings_minutes', 0.0),
|
||||
'cost': simulation_result.get('cost_savings', 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("vrp_simulation_failed", error=str(e))
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
Reference in New Issue
Block a user