Harshith
Initial commit of essential files for Gridlock Smart Patrol dashboard
e3351a1
Raw
History Blame Contribute Delete
16.6 kB
import random
import math
from datetime import datetime
from backend.routing import RoutingService
class PatrolOptimizer:
def __init__(self, data_engine, routing_service: RoutingService, geometry_router: RoutingService = None):
self.data_engine = data_engine
self.router = routing_service
self.geometry_router = geometry_router or routing_service
def _get_station_centroid(self, police_station: str) -> tuple:
"""Calculate the average coordinate of violations for a station to act as the station HQ."""
lats = []
lons = []
for r in self.data_engine.records:
if r['police_station'] == police_station:
lats.append(r['lat'])
lons.append(r['lon'])
if lats:
return (sum(lats) / len(lats), sum(lons) / len(lons))
# Default Bengaluru center if police station is not found
return (12.9716, 77.5946)
def optimize_routes(self,
police_station: str,
day_of_week: int,
hour: int,
num_vehicles: int = 2,
time_budget_min: float = 120.0,
clearance_time_min: float = 15.0,
use_predictions: bool = True,
predictor = None,
strategy: str = "balanced",
custom_hotspots: list = None,
cleared_hotspots: set = None) -> dict:
"""
Generate optimal routes for multiple patrol vehicles.
Args:
police_station (str): Station name (e.g. 'Upparpet')
day_of_week (int): 0-6
hour (int): 0-23
num_vehicles (int): Number of separate patrol routes to generate
time_budget_min (float): Shift duration in minutes
clearance_time_min (float): Time spent at each hotspot in minutes
use_predictions (bool): Whether to use ML predicted GCI or historical GCI
predictor (CongestionPredictor): Pre-loaded predictor instance
strategy (str): Routing strategy
custom_hotspots (list): List of custom manually reported hotspots
cleared_hotspots (set): Set of manually cleared hotspot IDs or grid cell keys
Returns:
dict: Structured routes data for visual rendering
"""
start_time = datetime.now()
# 1. Fetch grid congestion indices
if use_predictions and predictor is not None:
grid_data = predictor.predict_grid_gci(day_of_week, hour, police_station)
else:
grid_data = self.data_engine.compute_grid_gci(day_of_week, hour, police_station)
# Filter out cleared historical grid cells
if cleared_hotspots:
for key in list(grid_data.keys()):
if key in cleared_hotspots:
grid_data[key]['gci'] = 0.0
# 2. Get police station starting coordinates
hq_coord = self._get_station_centroid(police_station)
# 3. Filter and sort hotspots
# Convert grid data into a list of hotspots
hotspots = []
for key, info in grid_data.items():
if info['gci'] > 0.0:
hotspots.append({
'id': key,
'x': info['x'],
'y': info['y'],
'lat': info['lat'],
'lon': info['lon'],
'gci': info['gci'],
'count': info['count'],
'explanation': info.get('explanation', {})
})
# Append active custom hotspots
if custom_hotspots:
for ch in custom_hotspots:
ch_id = ch['id']
if cleared_hotspots and (ch_id in cleared_hotspots or f"{ch['x']}_{ch['y']}" in cleared_hotspots):
continue
hotspots.append({
'id': ch_id,
'x': ch['x'],
'y': ch['y'],
'lat': ch['lat'],
'lon': ch['lon'],
'gci': ch['gci'],
'count': ch.get('count', 1),
'explanation': ch.get('explanation', {
'priority_tier': ch.get('severity', 'High'),
'top_vehicle_type': ch.get('vehicle_type', 'Car'),
'top_violation_type': ch.get('violation_type', 'Obstruction'),
'peak_hour': hour,
'confidence': 'High (Manual Dispatch)',
'reason': f"Live Report: {ch.get('description', '')}"
})
})
# Sort by congestion index descending
strategy = strategy if strategy in {"max_impact", "fast_sweep", "balanced"} else "balanced"
hotspots = self._rank_hotspots_for_strategy(hotspots, hq_coord, strategy)
# Limit to top 40 hotspots to keep routing computations fast and relevant
hotspots = hotspots[:40]
routes = []
visited_ids = set()
# 4. Generate routes sequentially for each vehicle
for v_idx in range(num_vehicles):
# Exclude already visited hotspots
available_hotspots = [h for h in hotspots if h['id'] not in visited_ids]
if not available_hotspots:
break
# Run Genetic Algorithm for this vehicle
route_path, route_stats = self._run_genetic_algorithm(
hq_coord,
available_hotspots,
time_budget_min,
clearance_time_min,
strategy
)
# Record visited nodes to avoid duplicates across vehicles
for node in route_path:
visited_ids.add(node['id'])
road_geometry = self._build_route_geometry(hq_coord, route_path)
routes.append({
'vehicle_id': v_idx + 1,
'path': route_path,
'road_path': road_geometry['road_path'],
'legs': road_geometry['legs'],
'route_provider': road_geometry['provider'],
'is_road_geometry': road_geometry['is_road_geometry'],
'stats': route_stats
})
duration_ms = (datetime.now() - start_time).total_seconds() * 1000.0
impact_summary = self._build_impact_summary(hotspots, routes, time_budget_min)
return {
'police_station': police_station,
'strategy': strategy,
'hq_coordinate': hq_coord,
'routes': routes,
'impact_summary': impact_summary,
'computation_time_ms': round(duration_ms, 1)
}
def _build_route_geometry(self, hq_coord: tuple, route_path: list) -> dict:
if not route_path:
return {
'road_path': [],
'legs': [],
'provider': 'none',
'is_road_geometry': False
}
legs = []
road_path = []
provider = 'none'
is_road_geometry = False
current_coord = hq_coord
for node in route_path:
next_coord = (node['lat'], node['lon'])
leg = self.geometry_router.get_route(current_coord, next_coord)
provider = leg.get('provider', provider)
is_road_geometry = is_road_geometry or leg.get('is_road_geometry', False)
coords = leg.get('path_coords', [])
if not road_path:
road_path.extend(coords)
elif coords:
road_path.extend(coords[1:])
legs.append({
'from': [round(current_coord[0], 6), round(current_coord[1], 6)],
'to': [round(next_coord[0], 6), round(next_coord[1], 6)],
'distance_km': leg.get('distance_km', 0),
'duration_min': leg.get('duration_min', 0),
'provider': leg.get('provider', 'unknown'),
'is_road_geometry': leg.get('is_road_geometry', False),
'path_coords': coords
})
current_coord = next_coord
return {
'road_path': road_path if is_road_geometry else [],
'legs': legs,
'provider': provider,
'is_road_geometry': is_road_geometry
}
def _rank_hotspots_for_strategy(self, hotspots: list, hq_coord: tuple, strategy: str) -> list:
if strategy == "fast_sweep":
return sorted(hotspots, key=lambda x: (x.get('count', 0), x['gci']), reverse=True)
if strategy == "max_impact":
return sorted(hotspots, key=lambda x: x['gci'], reverse=True)
def balanced_score(h):
route = self.router.get_route(hq_coord, (h['lat'], h['lon']))
return h['gci'] / max(1.0, route['duration_min'])
return sorted(hotspots, key=balanced_score, reverse=True)
def _build_impact_summary(self, hotspots: list, routes: list, time_budget_min: float) -> dict:
total_hotspot_gci = sum(h['gci'] for h in hotspots)
cleared_ids = set()
cleared_gci = 0.0
cleared_count = 0
total_time = 0.0
for route in routes:
total_time += route['stats']['total_time_min']
for node in route['path']:
if node['id'] not in cleared_ids:
cleared_ids.add(node['id'])
cleared_gci += node['gci']
cleared_count += node.get('count', 0)
remaining_gci = max(0.0, total_hotspot_gci - cleared_gci)
reduction_pct = (cleared_gci / total_hotspot_gci * 100.0) if total_hotspot_gci else 0.0
avg_utilization = total_time / max(1.0, len(routes) * time_budget_min) * 100.0 if routes else 0.0
if reduction_pct >= 55.0:
note = "High-impact deployment: patrols clear most priority hotspot load in this window."
elif reduction_pct >= 30.0:
note = "Balanced deployment: meaningful hotspot relief while preserving patrol coverage."
elif reduction_pct > 0.0:
note = "Limited relief: increase units or time budget for stronger congestion reduction."
else:
note = "No clearable hotspots found under the selected constraints."
return {
'before_gci': round(total_hotspot_gci, 1),
'after_gci': round(remaining_gci, 1),
'cleared_gci': round(cleared_gci, 1),
'reduction_pct': round(reduction_pct, 1),
'cleared_hotspots': len(cleared_ids),
'estimated_records_impacted': cleared_count,
'avg_unit_utilization_pct': round(avg_utilization, 1),
'cleared_grid_ids': sorted(cleared_ids),
'recommendation': note
}
def _run_genetic_algorithm(self,
hq_coord: tuple,
hotspots: list,
budget: float,
clearance: float,
strategy: str) -> tuple:
"""Solves the Orienteering Problem using a Genetic Algorithm."""
if not hotspots:
return [], {'total_gci': 0, 'total_time_min': 0, 'hotspots_visited': 0}
# GA Hyperparameters
pop_size = 40
generations = 60
mutation_rate = 0.15
# Precompute distance/time from HQ to all nodes, and between all nodes to save compute cycles
# cache key: (coordA, coordB) -> (distance, time_min)
dist_cache = {}
def get_travel_time(coord1, coord2):
cache_key = (coord1, coord2)
if cache_key not in dist_cache:
route = self.router.get_route(coord1, coord2)
dist_cache[cache_key] = route['duration_min']
return dist_cache[cache_key]
# Helper: Decode chromosome (a permutation of indices) to a valid route
def decode(chromosome):
path = []
total_time = 0.0
total_gci = 0.0
total_count = 0
current_coord = hq_coord
for idx in chromosome:
h = hotspots[idx]
h_coord = (h['lat'], h['lon'])
# Calculate time to travel from current location to this hotspot
travel_time = get_travel_time(current_coord, h_coord)
# Check if visiting this hotspot fits in time budget
if total_time + travel_time + clearance <= budget:
total_time += travel_time + clearance
total_gci += h['gci']
total_count += h.get('count', 0)
path.append(h)
current_coord = h_coord
else:
# Once budget is exceeded, stop adding nodes (truncation)
break
if strategy == "fast_sweep":
fitness = len(path) * 35.0 + total_count * 2.0 - total_time * 0.15
elif strategy == "max_impact":
fitness = total_gci
else:
fitness = total_gci + len(path) * 8.0 - total_time * 0.35
return path, total_gci, total_time, fitness
# Initialize Population (random permutations of hotspot indices)
num_nodes = len(hotspots)
population = []
for _ in range(pop_size):
chrom = list(range(num_nodes))
random.shuffle(chrom)
population.append(chrom)
# GA Loop
for _ in range(generations):
# 1. Evaluate fitness
fitness_scores = []
for chrom in population:
_, _, _, fitness = decode(chrom)
fitness_scores.append(fitness)
# 2. Selection (Tournament Selection)
new_pop = []
for _ in range(pop_size):
# Sample 3 random individuals and select the best
candidates = random.sample(list(zip(population, fitness_scores)), 3)
best = max(candidates, key=lambda x: x[1])[0]
new_pop.append(best.copy())
# 3. Crossover (Ordered Crossover for permutations)
for i in range(0, pop_size, 2):
if i + 1 < pop_size and random.random() < 0.8:
p1, p2 = new_pop[i], new_pop[i+1]
# Crossover slice
cx1 = random.randint(0, num_nodes - 2)
cx2 = random.randint(cx1 + 1, num_nodes - 1)
# Child 1
c1 = [-1] * num_nodes
c1[cx1:cx2] = p1[cx1:cx2]
# Fill remainder from p2
p2_filtered = [item for item in p2 if item not in c1[cx1:cx2]]
c1[:cx1] = p2_filtered[:cx1]
c1[cx2:] = p2_filtered[cx1:]
# Child 2
c2 = [-1] * num_nodes
c2[cx1:cx2] = p2[cx1:cx2]
# Fill remainder from p1
p1_filtered = [item for item in p1 if item not in c2[cx1:cx2]]
c2[:cx1] = p1_filtered[:cx1]
c2[cx2:] = p1_filtered[cx1:]
new_pop[i], new_pop[i+1] = c1, c2
# 4. Mutation (Swap Mutation)
for i in range(pop_size):
if random.random() < mutation_rate:
m1 = random.randint(0, num_nodes - 1)
m2 = random.randint(0, num_nodes - 1)
new_pop[i][m1], new_pop[i][m2] = new_pop[i][m2], new_pop[i][m1]
population = new_pop
# Get the absolute best solution from final population
best_chrom = max(population, key=lambda chrom: decode(chrom)[3])
best_path, best_gci, best_time, _ = decode(best_chrom)
# Format the stats
stats = {
'total_gci': round(best_gci, 1),
'total_time_min': round(best_time, 1),
'hotspots_visited': len(best_path)
}
return best_path, stats