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)