Spaces:
Sleeping
Sleeping
File size: 5,347 Bytes
639f871 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """
ga/seeding.py β Inizializzazione della popolazione con greedy seeding.
Rispetta il TouristProfile in ogni costruzione:
- Filtra le categorie non ammesse
- Usa effective_score (con boost tag) nel criterio di selezione
- Usa travel_time_min del profilo per i tempi
"""
from __future__ import annotations
import random
from config import ROUTE_DETOUR_FACTOR
from core.models import Individual, PoI
from core.distance import DistanceMatrix, haversine_km
from core.profile import TouristProfile
from ga.repair import RepairEngine
class GreedySeeder:
def __init__(
self,
pois: list[PoI],
dm: DistanceMatrix,
repair: RepairEngine,
profile: TouristProfile,
start_time: int,
budget: int,
start_lat: float,
start_lon: float,
):
self.pois = pois
self.dm = dm
self.repair = repair
self.profile = profile
self.start_time = start_time
self.budget = budget
self.start_lat = start_lat
self.start_lon = start_lon
# Pool filtrato per categorie ammesse β usato in tutta la seeding
self.allowed_pois = [
p for p in pois
if profile.allows_category(p.category.value)
]
def build_population(self, pop_size: int) -> list[Individual]:
population = []
n_greedy = max(1, int(pop_size * 0.20))
n_perturbed = max(1, int(pop_size * 0.20))
n_random = pop_size - n_greedy - n_perturbed
for _ in range(n_greedy):
ind = self._greedy_construct(randomize=False, alpha=0.0)
ind = self.repair.repair(ind) # β cap snack/ristoranti anche sui greedy
population.append(ind)
for i in range(n_perturbed):
alpha = 0.15 + (i / n_perturbed) * 0.35
ind = self._greedy_construct(randomize=True, alpha=alpha)
ind = self.repair.repair(ind) # β idem
population.append(ind)
for _ in range(n_random):
shuffled = random.sample(self.allowed_pois, len(self.allowed_pois))
ind = Individual(genes=shuffled[:random.randint(1, len(shuffled))])
ind = self.repair.repair(ind)
population.append(ind)
return population
def _greedy_construct(self, randomize: bool = False, alpha: float = 0.0) -> Individual:
"""
Greedy con RCL. Usa group_overhead per coerenza con FitnessEvaluator.
Salta i ristoranti (li aggiunge _ensure_meal_slots) e riserva tempo
solo per gli slot pasto SERALI (β₯18:00), non per il pranzo β il pranzo
cade nel flusso naturale del tour diurno e viene inserito da
_ensure_meal_slots senza bisogno di riserva esplicita.
"""
group_extra = max(0, self.profile.group_size - 1) * 5
# Riserva tempo per ogni slot pasto che il greedy salta (tutti):
# - slot serali (β₯18:00): 90 min (cena dopo il tour diurno)
# - slot diurni (<18:00): 75 min (pranzo inserito nel mezzo del tour)
EVENING_RESERVE = 90
DAYTIME_RESERVE = 75
EVENING_THRESHOLD = 1080 # 18:00
total_reserve = sum(
EVENING_RESERVE if slot_open >= EVENING_THRESHOLD else DAYTIME_RESERVE
for (slot_open, _) in self.profile.needs_meal_slot()
)
effective_end = self.start_time + self.budget - total_reserve
tour = []
visited = set()
time_now = self.start_time
prev_lat = self.start_lat
prev_lon = self.start_lon
while True:
candidates = []
for poi in self.allowed_pois:
if poi.id in visited:
continue
if poi.category.value == "restaurant":
continue # ristoranti: aggiunti da _ensure_meal_slots
km = haversine_km(prev_lat, prev_lon, poi.lat, poi.lon) * ROUTE_DETOUR_FACTOR
travel_min = self.profile.travel_time_min(km)
arrival = time_now + travel_min
if arrival > poi.time_window.close:
continue
actual_arrival = max(arrival, poi.time_window.open)
duration = poi.visit_duration + group_extra
finish = actual_arrival + duration
if finish > effective_end:
continue
overhead = travel_min + max(0, poi.time_window.open - arrival)
eff_score = self.profile.effective_score(poi)
ratio = eff_score / (overhead + duration + 1e-9)
candidates.append((ratio, poi, actual_arrival, finish))
if not candidates:
break
candidates.sort(key=lambda x: x[0], reverse=True)
if randomize and len(candidates) > 1 and random.random() < alpha:
rcl_size = max(1, int(len(candidates) * 0.20))
_, poi, _, finish = random.choice(candidates[:rcl_size])
else:
_, poi, _, finish = candidates[0]
tour.append(poi)
visited.add(poi.id)
prev_lat = poi.lat
prev_lon = poi.lon
time_now = finish
return Individual(genes=tour) |