File size: 7,283 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
"""
demo_rome.py — Demo aggiornato: TouristProfile + fix ristoranti + tempi transit realistici.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.models import PoI, PoICategory, TimeWindow
from core.distance import DistanceMatrix
from core.profile import (
    profile_cultural_walker, profile_foodie_transit,
    profile_family_mixed, profile_art_lover_car,
    TouristProfile, TransportMode, MobilityLevel,
)
from solver import NSGA2Solver, SolverConfig

ROME_POIS = [
    # Monumenti e luoghi
    PoI("colosseo",   "Colosseo",             41.8902,12.4922,0.98,120,TimeWindow(540,1110),PoICategory.MONUMENT,  ["antico","unesco"]),
    PoI("foro",       "Foro Romano",           41.8925,12.4853,0.90, 90,TimeWindow(540,1110),PoICategory.MONUMENT,  ["antico"]),
    PoI("vaticano",   "Musei Vaticani",        41.9065,12.4534,0.97,180,TimeWindow(540,1080),PoICategory.MUSEUM,    ["arte","unesco"]),
    PoI("sistina",    "Cappella Sistina",      41.9029,12.4545,0.96, 60,TimeWindow(540,1080),PoICategory.MUSEUM,    ["arte","rinascimento"]),
    PoI("pantheon",   "Pantheon",              41.8986,12.4769,0.93, 60,TimeWindow(540,1140),PoICategory.MONUMENT,  ["antico","architettura"]),
    PoI("trevi",      "Fontana di Trevi",      41.9009,12.4833,0.88, 30,TimeWindow(0,  1440),PoICategory.MONUMENT,  ["barocco","fotogenico"]),
    PoI("spagna",     "Piazza di Spagna",      41.9059,12.4823,0.80, 30,TimeWindow(0,  1440),PoICategory.VIEWPOINT, ["shopping","fotogenico"]),
    PoI("borghese",   "Galleria Borghese",     41.9143,12.4923,0.92,120,TimeWindow(540,1140),PoICategory.MUSEUM,    ["arte","scultura"]),
    PoI("navona",     "Piazza Navona",         41.8992,12.4731,0.85, 45,TimeWindow(0,  1440),PoICategory.VIEWPOINT, ["barocco","fotogenico"]),
    PoI("trastevere", "Trastevere",            41.8897,12.4703,0.82, 90,TimeWindow(600,1380),PoICategory.VIEWPOINT, ["quartiere","fotogenico"]),
    PoI("castel",     "Castel Sant'Angelo",    41.9031,12.4663,0.83, 90,TimeWindow(540,1080),PoICategory.MONUMENT,  ["medievale","panorama"]),
    PoI("aventino",   "Giardino degli Aranci", 41.8837,12.4793,0.75, 40,TimeWindow(480,1200),PoICategory.PARK,      ["panorama","fotogenico"]),
    PoI("terme",      "Terme di Caracalla",    41.8788,12.4924,0.78, 75,TimeWindow(540,1080),PoICategory.MONUMENT,  ["antico"]),
    # Ristoranti: solo per pranzo (TW 12-15) o cena (TW 19-22)
    PoI("rist_rione", "Osteria del Rione",     41.8962,12.4751,0.74, 60,TimeWindow(720, 900),PoICategory.RESTAURANT,["cucina_romana"]),
    PoI("rist_prati", "Trattoria Prati",       41.9042,12.4601,0.70, 75,TimeWindow(720, 900),PoICategory.RESTAURANT,["cucina_romana"]),
    PoI("rist_testac","Testaccio da Mario",    41.8792,12.4770,0.76, 70,TimeWindow(720, 900),PoICategory.RESTAURANT,["offal","cucina_romana"]),
    PoI("rist_cena1", "Ristorante San Pietro", 41.9050,12.4580,0.72, 80,TimeWindow(1140,1320),PoICategory.RESTAURANT,["cucina_romana"]),
    PoI("rist_cena2", "Da Enzo al 29",         41.8891,12.4697,0.78, 90,TimeWindow(1140,1320),PoICategory.RESTAURANT,["cucina_romana","trastevere"]),
    # Bar e caffè: colazione o pausa (TW ampia)
    PoI("bar_greco",  "Caffè Greco",           41.9057,12.4818,0.72, 20,TimeWindow(480,1320),PoICategory.BAR,       ["storico","caffe"]),
    PoI("bar_sant",   "Sant'Eustachio il Caffè",41.8990,12.4752,0.75, 20,TimeWindow(480,1380),PoICategory.BAR,      ["caffe","storico"]),
    PoI("bar_campo",  "Bar del Fico",          41.8968,12.4720,0.65, 25,TimeWindow(600,1380),PoICategory.BAR,       ["aperitivo","vivace"]),
    # Gelaterie: pomeriggio (TW 11-20)
    PoI("gel_fatamorgana","Fatamorgana",       41.8993,12.4729,0.70, 20,TimeWindow(660,1200),PoICategory.GELATERIA, ["artigianale","insolito"]),
    PoI("gel_giolitti",   "Giolitti",          41.9003,12.4765,0.68, 20,TimeWindow(660,1260),PoICategory.GELATERIA, ["storico","classico"]),
    PoI("gel_prati",      "Fatamorgana Prati", 41.9090,12.4626,0.66, 20,TimeWindow(660,1260),PoICategory.GELATERIA, ["artigianale"]),
]


def profile_foodie_transit_updated() -> TouristProfile:
    """Gastronomico con mezzi: pranzo + cena, bar e gelateria nel pomeriggio."""
    return TouristProfile(
        transport_mode     = TransportMode.TRANSIT,
        mobility           = MobilityLevel.NORMAL,
        allowed_categories = ["restaurant", "bar", "gelateria", "monument", "viewpoint", "park"],
        want_lunch         = True,
        want_dinner        = True,
        lunch_time         = 720,   # 12:00
        dinner_time        = 1200,  # 20:00
        meal_window        = 60,
        tag_weights        = {"cucina_romana": 1.6, "offal": 0.5, "vivace": 1.2, "caffe": 1.1},
    )


def run_profile(name, profile, config, dm):
    print(f"\n{'━'*60}")
    print(f"  Profilo: {name}")
    print(f"{'━'*60}")
    print(profile.summary())
    dm.profile = profile
    solver = NSGA2Solver(ROME_POIS, dm, config, profile=profile)

    def cb(gen, pareto, stats):
        if gen % 30 == 0 or gen == 1:
            print(f"  gen {gen:3d} | pareto={stats['pareto_size']:2d} | "
                  f"best={stats['best_scalar']:.4f} | feasible={stats['feasible_pct']:.0f}%")

    front = solver.solve(callback=cb)
    feasible = [x for x in front if x.fitness.is_feasible] or front
    if not feasible:
        print("  Nessuna soluzione trovata.")
        return
    best = max(feasible, key=lambda x: x.fitness.scalar)
    sched = solver.evaluator.decode(best)

    # Conta categorie per verifica
    cats = {}
    for stop in sched.stops:
        k = stop.poi.category.value
        cats[k] = cats.get(k, 0) + 1

    print(f"\n  ★ Tour: {len(best.genes)} PoI | score={best.fitness.total_score:.2f} | "
          f"{best.fitness.total_distance:.1f}km | {best.fitness.total_time}min")
    print(f"     Composizione: {', '.join(f'{v}×{k}' for k,v in sorted(cats.items()))}")
    print()
    if sched:
        print(sched.summary())


def main():
    print("Costruzione matrice distanze...")
    dm = DistanceMatrix(ROME_POIS)
    dm.build()

    config = SolverConfig(
        pop_size         = 60,
        max_generations  = 200,
        budget           = 660,    # 11 ore (09:30–20:30)
        start_time       = 570,    # 09:30
        start_lat        = 41.896,
        start_lon        = 12.484,
        stagnation_limit = 25,
    )

    profiles = [
        ("Gastronomico con mezzi (aggiornato)", profile_foodie_transit_updated()),
        ("Culturale a piedi",                   profile_cultural_walker()),
        ("Gastronomico con mezzi (standard)", profile_foodie_transit()),
        ("Family: misto",                      profile_family_mixed()),
        ("Art Lover: con auto",                 profile_art_lover_car()),
        ("Custom: solo viste, no pasti",         TouristProfile(
            transport_mode=TransportMode.WALK,
            allowed_categories=["monument","viewpoint","park","bar","gelateria"],
            want_lunch=False, want_dinner=False,
            tag_weights={"fotogenico":1.5,"panorama":1.4,"caffe":1.1},
        )),
    ]

    for name, profile in profiles:
        run_profile(name, profile, config, dm)

    print(f"\n{'━'*60}\n  Completato.\n")


if __name__ == "__main__":
    main()