357 lines
12 KiB
Python
357 lines
12 KiB
Python
"""
|
|
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)
|
|
} |