Spaces:
Sleeping
Sleeping
File size: 11,594 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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | """
core/profile.py — Profilo del turista con tutte le preferenze personali.
È l'oggetto centrale che attraversa TUTTO il pipeline GA:
DistanceMatrix → velocità di spostamento per modalità
GreedySeeder → whitelist categorie + slot pasto garantito
RepairEngine → blacklist categorie + rimozione violazioni profilo
FitnessEvaluator → boost/malus score per tag di interesse
GeneticOperators → add/remove usa solo PoI ammessi dal profilo
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from config import (MIXED_THRESHOLD_M, TRANSIT_WALK_THRESHOLD_KM, SCORE_BOOST_CAP,
TRANSIT_OVERHEAD_MIN, CAR_OVERHEAD_MIN, SPEED_KMH,
MIXED_LONG_SPEED_KMH, WANT_LUNCH, WANT_DINNER, LUNCH_TIME,
DINNER_TIME, MEAL_WINDOW, MAX_BAR_STOPS, MAX_GELATERIA_STOPS)
class TransportMode(Enum):
WALK = "walk" # tutto a piedi (centro storico)
CAR = "car" # auto / taxi
TRANSIT = "transit" # bus + metro
MIXED = "mixed" # a piedi <MIXED_THRESHOLD_M, transit/auto oltre
class MobilityLevel(Enum):
NORMAL = "normal" # nessuna limitazione
LIMITED = "limited" # difficoltà con scale, distanze lunghe → penalizza PoI lontani
# Soglia in metri sotto la quale si va a piedi anche in modalità MIXED
#MIXED_THRESHOLD_M = 600
# Soglia al di sotto della quale, anche in modalità TRANSIT, si cammina:
# prendere un mezzo per 300m è spesso più lento che andare a piedi.
#TRANSIT_WALK_THRESHOLD_KM = 0.40
# Overhead fisso per ogni tratta in mezzo pubblico (minuti):
# comprende cammino alla fermata + attesa + cammino dalla fermata.
# Roma: metro ha frequenza ~5 min, bus ~8-12 min → media ~10 min overhead.
#TRANSIT_OVERHEAD_MIN = 10
# Overhead per auto/taxi: parcheggio + spostamento a piedi dal parcheggio.
#CAR_OVERHEAD_MIN = 5
# Velocità medie di percorrenza (escluso overhead)
# SPEED_TABLE: dict[tuple[TransportMode, MobilityLevel], float] = {
# (TransportMode.WALK, MobilityLevel.NORMAL): 4.5,
# (TransportMode.WALK, MobilityLevel.LIMITED): 3.0,
# (TransportMode.CAR, MobilityLevel.NORMAL): 25.0,
# (TransportMode.CAR, MobilityLevel.LIMITED): 25.0,
# (TransportMode.TRANSIT, MobilityLevel.NORMAL): 20.0, # velocità effettiva metro/bus Roma
# (TransportMode.TRANSIT, MobilityLevel.LIMITED): 16.0,
# (TransportMode.MIXED, MobilityLevel.NORMAL): 4.5, # usata per segmenti brevi
# (TransportMode.MIXED, MobilityLevel.LIMITED): 3.0,
# }
# MIXED_LONG_SPEED: dict[MobilityLevel, float] = {
# MobilityLevel.NORMAL: 20.0,
# MobilityLevel.LIMITED: 16.0,
# }
@dataclass
class TouristProfile:
"""
Preferenze complete del turista.
Tutti i campi hanno un default sensato per un turista generico.
"""
# --- Trasporto ---
transport_mode: TransportMode = TransportMode.WALK
mobility: MobilityLevel = MobilityLevel.NORMAL
# --- Categorie ammesse ---
# Se una categoria non è in questa lista, i suoi PoI vengono
# completamente ignorati in seeding, repair e mutation.
allowed_categories: list[str] = field(default_factory=lambda: [
"museum", "monument", "restaurant", "park", "viewpoint"
])
# --- Preferenze pasti ---
want_lunch: bool = WANT_LUNCH
want_dinner: bool = WANT_DINNER
lunch_time: int = LUNCH_TIME
dinner_time: int = DINNER_TIME
meal_window: int = MEAL_WINDOW
# --- Soste snack (bar, gelateria) ---
# Numero massimo di soste snack per tipo nel tour.
# None = nessun limite (utile per profili food-focused).
max_bar_stops: int = MAX_BAR_STOPS
max_gelateria_stops: int = MAX_GELATERIA_STOPS
# --- Interessi tematici (tag) ---
# Ogni tag elencato riceve un boost moltiplicativo allo score del PoI.
# Es. {"arte": 1.5, "antico": 1.3} → i musei d'arte valgono 50% di più.
tag_weights: dict[str, float] = field(default_factory=dict)
# --- Budget economico ---
max_entry_fee: Optional[float] = None # euro; None = nessun limite
# --- Gruppo ---
group_size: int = 1 # utile per entry fee totale e ritmo di visita
def __post_init__(self):
# Normalizza le categorie in minuscolo
self.allowed_categories = [c.lower() for c in self.allowed_categories]
# Coerci transport_mode da stringa a enum se necessario
if isinstance(self.transport_mode, str):
self.transport_mode = TransportMode(self.transport_mode.lower())
# Coerci mobility da stringa a enum se necessario
if isinstance(self.mobility, str):
self.mobility = MobilityLevel(self.mobility.lower())
def allows_category(self, category_value: str) -> bool:
"""Restituisce True se la categoria è ammessa dal profilo."""
return category_value.lower() in self.allowed_categories
def effective_score(self, poi) -> float:
"""
Score del PoI moltiplicato per i boost dei tag di interesse.
Un PoI senza tag corrispondenti mantiene lo score base.
"""
boost = 1.0
for tag in poi.tags:
if tag in self.tag_weights:
boost += self.tag_weights[tag] - 1.0 # additive boost
return min(poi.score * boost, SCORE_BOOST_CAP)
def travel_speed_kmh(self, dist_km: float) -> float:
"""Velocità di percorrenza pura (senza overhead fisso)."""
if self.transport_mode == TransportMode.MIXED:
dist_m = dist_km * 1000
if dist_m <= MIXED_THRESHOLD_M:
return SPEED_KMH[(TransportMode.WALK.value, self.mobility.value)]
else:
return MIXED_LONG_SPEED_KMH[self.mobility]
return SPEED_KMH.get((self.transport_mode.value, self.mobility.value))
def travel_time_min(self, dist_km: float) -> int:
"""
Tempo di percorrenza realistico in minuti.
Modello per modalità:
WALK → dist / v_walk
CAR → dist / v_car + CAR_OVERHEAD (parcheggio)
TRANSIT → se dist < soglia: a piedi (prendere il mezzo non conviene)
altrimenti: dist / v_transit + TRANSIT_OVERHEAD (attesa + fermata)
MIXED → a piedi se dist < MIXED_THRESHOLD, altrimenti come TRANSIT
"""
mode = self.transport_mode
walk_speed = SPEED_KMH[(TransportMode.WALK.value, self.mobility.value)]
if mode == TransportMode.WALK:
return max(1, int((dist_km / walk_speed) * 60))
if mode == TransportMode.CAR:
speed = SPEED_KMH.get((TransportMode.CAR.value, self.mobility.value))
return max(3, int((dist_km / speed) * 60) + CAR_OVERHEAD_MIN)
if mode == TransportMode.TRANSIT:
if dist_km < TRANSIT_WALK_THRESHOLD_KM:
# Distanza troppo corta: a piedi è più veloce del mezzo
return max(1, int((dist_km / walk_speed) * 60))
speed = SPEED_KMH[(TransportMode.TRANSIT.value, self.mobility.value)]
ride = int((dist_km / speed) * 60)
return ride + TRANSIT_OVERHEAD_MIN
if mode == TransportMode.MIXED:
dist_m = dist_km * 1000
if dist_m <= MIXED_THRESHOLD_M:
return max(1, int((dist_km / walk_speed) * 60))
long_speed = MIXED_LONG_SPEED_KMH[self.mobility.value]
ride = int((dist_km / long_speed) * 60)
return ride + TRANSIT_OVERHEAD_MIN
# Fallback
return max(1, int((dist_km / walk_speed) * 60))
def needs_meal_slot(self) -> list[tuple[int, int]]:
"""
Restituisce la lista di finestre temporali in cui il profilo
richiede un ristorante nel tour.
Es. [(660, 780), (1080, 1200)] per pranzo+cena.
"""
slots = []
if self.want_lunch:
slots.append((
self.lunch_time - self.meal_window,
self.lunch_time + self.meal_window
))
if self.want_dinner:
slots.append((
self.dinner_time - self.meal_window,
self.dinner_time + self.meal_window
))
return slots
def summary(self) -> str:
lines = [
f"Trasporto : {self.transport_mode.value} | Mobilità: {self.mobility.value}",
f"Categorie : {', '.join(self.allowed_categories)}",
f"Pranzo : {'sì (' + str(self.lunch_time//60) + ':00)' if self.want_lunch else 'no'} | "
f"Cena: {'sì (' + str(self.dinner_time//60) + ':00)' if self.want_dinner else 'no'}",
]
if self.tag_weights:
tw = ", ".join(f"{k}×{v}" for k, v in self.tag_weights.items())
lines.append(f"Interessi : {tw}")
if self.max_entry_fee is not None:
lines.append(f"Budget biglietti: €{self.max_entry_fee:.0f} max a PoI")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Factory: profili predefiniti pronti all'uso
# ---------------------------------------------------------------------------
#TODO: rivedere la definizione di tutti i profili e i pesi, facendo attenzione alle allowed_categories e alle tag_weights per coerenza interna.
def profile_cultural_walker() -> TouristProfile:
"""Turista culturale a piedi, interessato ad arte e storia. Include una sosta pranzo."""
return TouristProfile(
transport_mode = TransportMode.WALK,
allowed_categories = ["museum", "monument", "viewpoint", "restaurant", "bar", "gelateria"],
want_lunch = True,
want_dinner = False,
tag_weights = {"arte": 1.4, "antico": 1.3, "rinascimento": 1.5, "unesco": 1.2},
max_bar_stops = 1,
max_gelateria_stops= 1
)
def profile_foodie_transit() -> TouristProfile:
"""Turista gastronomico con mezzi pubblici, ristoranti inclusi."""
return TouristProfile(
transport_mode = TransportMode.TRANSIT,
allowed_categories = ["restaurant", "monument", "bar", "gelateria", "viewpoint", "park"],
want_lunch = True,
want_dinner = True,
tag_weights = {"cucina_romana": 1.6, "offal": 0.5, "vivace": 1.2},
max_bar_stops = 2,
max_gelateria_stops= 2
)
def profile_family_mixed() -> TouristProfile:
"""Famiglia con bambini: percorso misto, evita musei pesanti."""
return TouristProfile(
transport_mode = TransportMode.MIXED,
mobility = MobilityLevel.LIMITED,
allowed_categories = ["monument", "park", "viewpoint", "restaurant", "bar", "gelateria"],
want_lunch = True,
want_dinner = False,
group_size = 4,
tag_weights = {"fotogenico": 1.3, "vivace": 1.2},
max_entry_fee = 15.0,
max_bar_stops = 1,
max_gelateria_stops= 1
)
def profile_art_lover_car() -> TouristProfile:
"""Appassionato d'arte con auto: vuole visitare musei anche lontani."""
return TouristProfile(
transport_mode = TransportMode.CAR,
allowed_categories = ["museum", "monument", "bar", "gelateria"],
want_lunch = True,
want_dinner = False,
tag_weights = {"arte": 1.6, "scultura": 1.5, "rinascimento": 1.4, "antico": 1.1},
max_entry_fee = 30.0,
max_bar_stops = 1,
max_gelateria_stops= 1
) |