Tour_Generator_GA / ga /seeding.py
GaetanoParente's picture
first commit
639f871
"""
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)