Skip to content

Commit 9349b84

Browse files
authored
Update matching_engine.py
1 parent 4b79246 commit 9349b84

File tree

1 file changed

+109
-43
lines changed

1 file changed

+109
-43
lines changed

matching_engine.py

Lines changed: 109 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,132 @@
11
from typing import List, Optional
2-
from src.common.models import RideRequest, DriverLocation, MatchResult
3-
from src.common.utils import haversine_distance, utc_now, generate_id
2+
from math import sqrt
3+
4+
from src.common.models import DriverLocationEvent, TripRequestEvent, MatchResultEvent
5+
from src.common.utils import get_logger, now_timestamp
46

57

68
class MatchingEngine:
79
"""
8-
Core matching logic for assigning drivers to ride requests.
9-
Production-ready, easily extendable for ML models or geospatial indexing.
10+
Core driver–rider matching algorithm.
11+
12+
This version uses:
13+
- Euclidean distance heuristic
14+
- Simple ETA estimation
15+
- Surge-aware scoring
16+
- Top-K candidate ranking
17+
18+
In real systems (Uber, Lyft), this would expand into:
19+
- H3 geospatial indexing
20+
- ML-based ETA prediction
21+
- Supply/demand balancing
1022
"""
1123

12-
def __init__(self):
13-
# In production this would be from Redis, Cassandra, DynamoDB, etc.
14-
self.active_drivers: List[DriverLocation] = []
24+
def __init__(self, max_candidates: int = 25):
25+
self.max_candidates = max_candidates
26+
self.logger = get_logger("MatchingEngine")
1527

16-
def update_driver_location(self, driver: DriverLocation):
28+
# ------------------------------------------------------------
29+
# Distance Heuristic
30+
# ------------------------------------------------------------
31+
32+
def _distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
1733
"""
18-
Add or update driver info in memory. This would normally come from Kafka.
34+
Simple Euclidean distance for demo purposes.
35+
Replace with Haversine or H3 index for production.
1936
"""
20-
# Remove existing driver entry
21-
self.active_drivers = [d for d in self.active_drivers if d.driver_id != driver.driver_id]
22-
self.active_drivers.append(driver)
37+
return sqrt((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2)
38+
39+
# ------------------------------------------------------------
40+
# ETA Estimation
41+
# ------------------------------------------------------------
2342

24-
def find_best_driver(self, request: RideRequest) -> Optional[MatchResult]:
43+
def _estimate_eta(self, distance: float) -> float:
2544
"""
26-
Select the closest available driver using Haversine distance.
45+
ETA calculation: distance * factor
46+
In real systems: ML model or traffic-aware routing system.
2747
"""
28-
available_drivers = [d for d in self.active_drivers if d.is_available]
48+
return round(distance * 4, 2) # 4 minutes per distance unit heuristic
2949

30-
if not available_drivers:
31-
return None
50+
# ------------------------------------------------------------
51+
# Score Function
52+
# ------------------------------------------------------------
53+
54+
def _score_driver(self, distance: float, surge_multiplier: float) -> float:
55+
"""
56+
Higher score = better match.
57+
Lower distance = higher score.
58+
Surge slightly boosts score to balance rider demand.
59+
"""
60+
return max(0.01, (1 / (distance + 0.01))) * surge_multiplier
61+
62+
# ------------------------------------------------------------
63+
# Candidate Ranking
64+
# ------------------------------------------------------------
65+
66+
def rank_drivers(
67+
self,
68+
drivers: List[DriverLocationEvent],
69+
trip: TripRequestEvent,
70+
surge_multiplier: float
71+
) -> List[DriverLocationEvent]:
72+
"""
73+
Sort drivers by score (descending).
74+
"""
75+
scored = []
3276

33-
# Compute distances
34-
scored_drivers = []
35-
for driver in available_drivers:
36-
distance = haversine_distance(
37-
request.pickup_lat,
38-
request.pickup_lng,
39-
driver.lat,
40-
driver.lng,
77+
for d in drivers:
78+
dist = self._distance(
79+
d.lat, d.lon,
80+
trip.pickup_lat, trip.pickup_lon
4181
)
4282

43-
# ETA estimation: assume average 30 km/h → 0.5 km/min
44-
eta_minutes = distance / 0.5 if distance > 0 else 1
45-
scored_drivers.append((driver, distance, eta_minutes))
83+
score = self._score_driver(dist, surge_multiplier)
84+
scored.append((score, d))
85+
86+
ranked = sorted(scored, key=lambda x: x[0], reverse=True)
87+
top_ranked = [d for _, d in ranked[: self.max_candidates]]
4688

47-
# Pick closest driver
48-
scored_drivers.sort(key=lambda x: x[1]) # sort by distance
89+
self.logger.info(f"Ranked {len(top_ranked)} drivers for trip {trip.rider_id}")
90+
return top_ranked
4991

50-
best_driver, best_distance, best_eta = scored_drivers[0]
92+
# ------------------------------------------------------------
93+
# Match Selection
94+
# ------------------------------------------------------------
5195

52-
# Create match result
53-
match = MatchResult(
54-
match_id=generate_id(),
55-
request_id=request.request_id,
56-
driver_id=best_driver.driver_id,
57-
eta_minutes=round(best_eta, 2),
58-
distance_km=round(best_distance, 2),
59-
surge_multiplier=request.surge_multiplier,
60-
timestamp=utc_now(),
96+
def select_best_match(
97+
self,
98+
drivers: List[DriverLocationEvent],
99+
trip: TripRequestEvent,
100+
surge_multiplier: float
101+
) -> Optional[MatchResultEvent]:
102+
"""
103+
Selects the highest-ranked driver and returns a MatchResultEvent.
104+
"""
105+
if not drivers:
106+
self.logger.warning("No available drivers for matching.")
107+
return None
108+
109+
ranked = self.rank_drivers(drivers, trip, surge_multiplier)
110+
best = ranked[0]
111+
112+
distance = self._distance(
113+
best.lat, best.lon,
114+
trip.pickup_lat, trip.pickup_lon
61115
)
116+
eta = self._estimate_eta(distance)
62117

63-
# Mark driver as unavailable
64-
best_driver.is_available = False
118+
match_event = MatchResultEvent(
119+
trip_id=f"trip_{trip.rider_id}_{now_timestamp().timestamp()}",
120+
driver_id=best.driver_id,
121+
rider_id=trip.rider_id,
122+
eta_minutes=eta,
123+
surge_multiplier=surge_multiplier,
124+
timestamp=now_timestamp(),
125+
)
126+
127+
self.logger.info(
128+
f"Selected driver {best.driver_id} for rider {trip.rider_id} "
129+
f"(ETA: {eta} mins, Surge: {surge_multiplier})"
130+
)
65131

66-
return match
132+
return match_event

0 commit comments

Comments
 (0)