initial project setup with README and ignore
This commit is contained in:
158
app/services/routing/realistic_eta_calculator.py
Normal file
158
app/services/routing/realistic_eta_calculator.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Realistic ETA Calculator for Delivery Operations
|
||||
|
||||
Accounts for:
|
||||
- City traffic conditions
|
||||
- Stop time at pickup/delivery
|
||||
- Navigation time
|
||||
- Parking/finding address time
|
||||
- Different speeds for different order types
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RealisticETACalculator:
|
||||
"""
|
||||
Calculates realistic ETAs accounting for real-world delivery conditions.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
from app.config.dynamic_config import get_config
|
||||
cfg = get_config()
|
||||
|
||||
# BASE SPEED (km/h) - Driven by the DB configuration
|
||||
base_speed = cfg.get("avg_speed_kmh", 18.0)
|
||||
|
||||
# REALISTIC SPEEDS based on time of day
|
||||
self.CITY_SPEED_HEAVY_TRAFFIC = base_speed * 0.7 # Usually ~12 km/h
|
||||
self.CITY_SPEED_MODERATE = base_speed # Usually ~18 km/h
|
||||
self.CITY_SPEED_LIGHT = base_speed * 1.2 # Usually ~21.6 km/h
|
||||
|
||||
# TIME BUFFERS (minutes)
|
||||
self.PICKUP_TIME = cfg.get("eta_pickup_time_min", 3.0)
|
||||
self.DELIVERY_TIME = cfg.get("eta_delivery_time_min", 4.0)
|
||||
self.NAVIGATION_BUFFER = cfg.get("eta_navigation_buffer_min", 1.5)
|
||||
|
||||
# DISTANCE-BASED SPEED SELECTION
|
||||
# Short distances (<2km) are slower due to more stops/starts
|
||||
# Long distances (>8km) might have highway portions
|
||||
self.SHORT_TRIP_FACTOR = cfg.get("eta_short_trip_factor", 0.8)
|
||||
self.LONG_TRIP_FACTOR = cfg.get("eta_long_trip_factor", 1.1)
|
||||
|
||||
def calculate_eta(self,
|
||||
distance_km: float,
|
||||
is_first_order: bool = False,
|
||||
order_type: str = "Economy",
|
||||
time_of_day: str = "peak") -> int:
|
||||
"""
|
||||
Calculate realistic ETA in minutes.
|
||||
|
||||
Args:
|
||||
distance_km: Distance to travel in kilometers
|
||||
is_first_order: If True, includes pickup time
|
||||
order_type: "Economy", "Premium", or "Risky"
|
||||
time_of_day: "peak", "normal", or "light" traffic
|
||||
|
||||
Returns:
|
||||
ETA in minutes (rounded up for safety)
|
||||
"""
|
||||
|
||||
if distance_km <= 0:
|
||||
return 0
|
||||
|
||||
# 1. SELECT SPEED BASED ON CONDITIONS
|
||||
if time_of_day == "peak":
|
||||
base_speed = self.CITY_SPEED_HEAVY_TRAFFIC
|
||||
elif time_of_day == "light":
|
||||
base_speed = self.CITY_SPEED_LIGHT
|
||||
else:
|
||||
base_speed = self.CITY_SPEED_MODERATE
|
||||
|
||||
# 2. ADJUST SPEED BASED ON DISTANCE
|
||||
# Short trips are slower (more intersections, traffic lights)
|
||||
if distance_km < 2.0:
|
||||
effective_speed = base_speed * self.SHORT_TRIP_FACTOR
|
||||
elif distance_km > 8.0:
|
||||
effective_speed = base_speed * self.LONG_TRIP_FACTOR
|
||||
else:
|
||||
effective_speed = base_speed
|
||||
|
||||
# 3. CALCULATE TRAVEL TIME
|
||||
travel_time = (distance_km / effective_speed) * 60 # Convert to minutes
|
||||
|
||||
# 4. ADD BUFFERS
|
||||
total_time = travel_time
|
||||
|
||||
# Pickup time (only for first order in sequence)
|
||||
if is_first_order:
|
||||
total_time += self.PICKUP_TIME
|
||||
|
||||
# Delivery time (always)
|
||||
total_time += self.DELIVERY_TIME
|
||||
|
||||
# Navigation buffer (proportional to distance)
|
||||
if distance_km > 3.0:
|
||||
total_time += self.NAVIGATION_BUFFER
|
||||
|
||||
# 5. SAFETY MARGIN (Round up to next minute)
|
||||
# Riders prefer to arrive early than late
|
||||
eta_minutes = int(total_time) + 1
|
||||
|
||||
return eta_minutes
|
||||
|
||||
def calculate_batch_eta(self, orders: list) -> list:
|
||||
"""
|
||||
Calculate ETAs for a batch of orders in sequence.
|
||||
|
||||
Args:
|
||||
orders: List of order dicts with 'previouskms' and 'step' fields
|
||||
|
||||
Returns:
|
||||
Same list with updated 'eta' fields
|
||||
"""
|
||||
for order in orders:
|
||||
distance_km = float(order.get('previouskms', 0))
|
||||
step = order.get('step', 1)
|
||||
order_type = order.get('ordertype', 'Economy')
|
||||
|
||||
# First order includes pickup time
|
||||
is_first = (step == 1)
|
||||
|
||||
# Assume peak traffic for safety (can be made dynamic)
|
||||
eta = self.calculate_eta(
|
||||
distance_km=distance_km,
|
||||
is_first_order=is_first,
|
||||
order_type=order_type,
|
||||
time_of_day="normal" # Default to moderate traffic
|
||||
)
|
||||
|
||||
order['eta'] = str(eta)
|
||||
order['eta_realistic'] = True # Flag to indicate realistic calculation
|
||||
|
||||
return orders
|
||||
|
||||
|
||||
def get_time_of_day_category() -> str:
|
||||
"""
|
||||
Determine current traffic conditions based on time.
|
||||
|
||||
Returns:
|
||||
"peak", "normal", or "light"
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
# Peak hours: 8-10 AM, 12-2 PM, 5-8 PM
|
||||
if (8 <= current_hour < 10) or (12 <= current_hour < 14) or (17 <= current_hour < 20):
|
||||
return "peak"
|
||||
# Light traffic: Late night/early morning
|
||||
elif current_hour < 7 or current_hour >= 22:
|
||||
return "light"
|
||||
else:
|
||||
return "normal"
|
||||
Reference in New Issue
Block a user