Files
bakery-ia/services/distribution/app/services/vrp_optimization_service.py
2025-12-17 20:50:22 +01:00

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)
}