File size: 22,834 Bytes
c8fa249
 
 
 
 
 
 
 
 
 
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
---
title: Tour Generator GA
emoji: 🚀
colorFrom: red
colorTo: purple
sdk: docker
pinned: false
license: apache-2.0
---

# tour_ga — Generatore di tour turistici con NSGA-II e TOP-TW

Progetto Python per la generazione automatica di tour personalizzati in città (Roma come caso di studio) tramite un algoritmo genetico multi-obiettivo. Il problema è modellato come **Team Orienteering Problem with Time Windows (TOP-TW)** e risolto con una variante di **NSGA-II** (Non-dominated Sorting Genetic Algorithm II, Deb et al., 2002).

---

## Indice

1. [Il problema: TOP-TW](#1-il-problema-top-tw)
2. [Fondamenti teorici: NSGA-II](#2-fondamenti-teorici-nsga-ii)
3. [Struttura del progetto](#3-struttura-del-progetto)
4. [Modello dati](#4-modello-dati)
5. [Profilo turista (TouristProfile)](#5-profilo-turista-touristprofile)
6. [Funzione fitness multi-obiettivo](#6-funzione-fitness-multi-obiettivo)
7. [Operatori genetici](#7-operatori-genetici)
8. [Riparazione genetica (Repair Engine)](#8-riparazione-genetica-repair-engine)
9. [Greedy Seeding](#9-greedy-seeding)
10. [Modello di trasporto realistico](#10-modello-di-trasporto-realistico)
11. [Configurazione e avvio](#11-configurazione-e-avvio)
12. [Risultati di esempio](#12-risultati-di-esempio)
13. [Riferimenti](#13-riferimenti)

---

## 1. Il problema: TOP-TW

Il **Team Orienteering Problem with Time Windows** è una generalizzazione del Travelling Salesman Problem in cui:

- Esiste un insieme di **Punti di Interesse (PoI)** con uno score di attrattività, una durata di visita stimata e una finestra temporale di apertura `[open, close]`.
- Non tutti i PoI possono essere visitati entro il **budget di tempo** giornaliero.
- L'obiettivo è selezionare e ordinare un sottoinsieme di PoI in modo da **massimizzare lo score totale** rispettando i vincoli temporali.

In questo progetto il problema viene esteso con tre obiettivi competitivi simultanei:

| Obiettivo | Direzione | Significato |
|-----------|-----------|-------------|
| `total_score` | Massimizza | Interesse culturale/gastronomico del tour |
| `total_distance` | Minimizza | Fatica e costi di spostamento |
| `time_penalty` | Minimizza | Sforamento del budget giornaliero |

La natura multi-obiettivo rende la ricerca di un'unica soluzione ottima impossibile: esistono molteplici soluzioni di compromesso (fronte di Pareto) tra le quali il turista sceglie in base alle proprie preferenze.

---

## 2. Fondamenti teorici: NSGA-II

L'algoritmo NSGA-II (Deb, Pratap, Agarwal, Meyarivan — *IEEE Transactions on Evolutionary Computation*, Vol. 6, No. 2, 2002) risolve i tre limiti principali del suo predecessore NSGA:

1. **Complessità computazionale** — Il sorting non-dominato originale era O(MN³). NSGA-II lo riduce a **O(MN²)** con un algoritmo di conteggio della dominanza.
2. **Mancanza di elitismo** — NSGA-II combina popolazione corrente e figli in un pool di 2N individui e seleziona i migliori N: le soluzioni eccellenti non vengono mai perse.
3. **Parametro di sharing** — Sostituito dalla **crowding distance**, una metrica parameter-free che misura la densità delle soluzioni nell'intorno di un individuo nello spazio degli obiettivi.

### 2.1 Fast Non-Dominated Sorting

Per ogni individuo `i` si calcolano:
- `dom_count[i]`: numero di individui che dominano `i`
- `dom_set[i]`: insieme degli individui dominati da `i`

Un individuo `A` **domina** `B` se è migliore o uguale su tutti gli obiettivi e strettamente migliore su almeno uno. Gli individui con `dom_count = 0` formano il **Fronte 1** (Pareto front). Rimuovendo il fronte 1 e ripetendo si ottengono i fronti successivi. La complessità totale è **O(MN²)**.

### 2.2 Crowding Distance

Per ogni fronte, la crowding distance di un individuo `i` è la somma delle distanze normalizzate tra i vicini immediati lungo ciascun obiettivo:

```
CD(i) = Σ_m  |f_m(i+1) - f_m(i-1)| / (f_m_max - f_m_min)
```

Gli individui agli estremi del fronte ricevono distanza infinita. La crowding distance alta indica un individuo in una regione poco affollata — preferito nella selezione per mantenere diversità.

### 2.3 Crowded-Comparison Operator

Nella selezione a torneo, un individuo `A` è preferito a `B` se:
- `rank(A) < rank(B)` (rango Pareto migliore), oppure
- `rank(A) == rank(B)` e `crowd(A) > crowd(B)` (regione meno affollata)

### 2.4 Ciclo evolutivo principale

```
Inizializzazione popolazione P (greedy seeding + casuale riparato)

Valutazione fitness (3 obiettivi)

Fast non-dominated sort → assegna rank e crowding distance

┌─────────────────────────────────────────────────────────┐
│  Selezione torneo (crowded-comparison)                  │
│  Crossover (OX | PoI-aware, prob cx_prob)               │
│  Mutazione (swap | insert | reverse | add-remove)       │
│  Riparazione (categoria → TW → budget → cap → pasto)   │
│  → popolazione figli Q (size N)                         │
└─────────────────────────────────────────────────────────┘

Unione R = P ∪ Q  (size 2N)

Non-dominated sort su R → seleziona migliori N → nuova P

Criterio di stop? (max generazioni | stagnazione)
        ↓ NO                              ↓ SÌ
     (loop)                     Restituisce Fronte 1
```

### 2.5 Adattamenti al TOP-TW

L'NSGA-II standard opera su cromosomi di lunghezza fissa con variabili continue. Per il TOP-TW sono stati introdotti:

- **Cromosomi a lunghezza variabile** — lista ordinata di PoI (sottoinsieme del pool, non permutazione completa)
- **Repair-before-evaluation** — ogni individuo viene riparato dopo ogni operatore genetico, prima della valutazione fitness
- **Gene Jolly** (via `add_remove_mutation`) — placeholder che viene sostituito con il PoI più conveniente disponibile
- **Penalità dinamica sulle attese** — scoraggia tour con buchi temporali lunghi

---

## 3. Struttura del progetto

```
tour_generator_ga/
├── core/
│   ├── models.py       # PoI, Individual, TourSchedule, FitnessScore
│   ├── distance.py     # DistanceMatrix (Haversine × 1.3)
│   ├── fitness.py      # FitnessEvaluator multi-obiettivo
│   └── profile.py      # TouristProfile + modello trasporto realistico
├── ga/
│   ├── operators.py    # crossover (OX, PoI-aware), mutation, selection
│   ├── repair.py       # RepairEngine: 7 step di riparazione
│   └── seeding.py      # GreedySeeder: costruzione iniziale α-greedy
├── data/               # (placeholder per loader OSM/CSV)
├── solver.py           # NSGA2Solver: ciclo evolutivo completo
├── demo_rome.py        # Demo con dataset Roma e confronto profili
└── README.md
```

---

## 4. Modello dati

### `PoI`

```python
@dataclass
class PoI:
    id:             str
    name:           str
    lat, lon:       float
    score:          float           # attrattività [0, 1]
    visit_duration: int             # minuti di visita
    time_window:    TimeWindow      # (open, close) in minuti dalla mezzanotte
    category:       PoICategory     # MUSEUM | MONUMENT | RESTAURANT | BAR | GELATERIA | PARK | VIEWPOINT
    tags:           list[str]       # es. ["arte", "antico", "fotogenico"]
```

### `Individual` (cromosoma)

```python
class Individual:
    genes:    list[PoI]       # sequenza ordinata del tour
    fitness:  FitnessScore    # calcolata dopo evaluate()
    _schedule: TourSchedule   # cache dello schedule decodificato
```

La rappresentazione come lista ordinata permette di codificare naturalmente sia l'insieme dei PoI visitati sia l'ordine di visita, senza vincoli di lunghezza fissa.

### `TourSchedule`

Output della decodifica del cromosoma: per ogni PoI vengono calcolati orario di arrivo, attesa all'apertura e orario di partenza. Il campo `total_wait` traccia i minuti di attesa cumulati e contribuisce alla penalità fitness.

### `PoICategory`

```
MUSEUM | MONUMENT | RESTAURANT | BAR | GELATERIA | PARK | VIEWPOINT
```

La distinzione tra `RESTAURANT` (pasto formale) e `BAR`/`GELATERIA` (sosta breve) permette al `RepairEngine` di applicare vincoli differenziati per tipo di sosta alimentare.

---

## 5. Profilo turista (TouristProfile)

Il `TouristProfile` è l'oggetto centrale che attraversa l'intero pipeline e determina il comportamento di ogni componente.

```python
@dataclass
class TouristProfile:
    transport_mode:      TransportMode     # WALK | CAR | TRANSIT | MIXED
    mobility:            MobilityLevel     # NORMAL | LIMITED
    allowed_categories:  list[str]         # categorie PoI ammesse
    want_lunch:          bool              # richiede sosta pranzo
    want_dinner:         bool              # richiede sosta cena
    lunch_time:          int               # orario preferito pranzo (minuti)
    dinner_time:         int               # orario preferito cena (minuti)
    meal_window:         int               # flessibilità ±minuti attorno all'orario
    max_bar_stops:       int = 1           # max soste bar nel tour
    max_gelateria_stops: int = 1           # max soste gelateria nel tour
    tag_weights:         dict[str, float]  # boost per tag di interesse
    max_entry_fee:       float | None      # budget biglietti per PoI
    group_size:          int = 1           # persone (influenza durata visite)
```

**Profili predefiniti disponibili:**

| Factory | Modalità | Categorie | Pasti |
|---------|----------|-----------|-------|
| `profile_cultural_walker()` | WALK | museum, monument, viewpoint, restaurant | pranzo |
| `profile_foodie_transit()` | TRANSIT | restaurant, bar, gelateria, monument, viewpoint | pranzo + cena |
| `profile_family_mixed()` | MIXED / LIMITED | monument, park, viewpoint, restaurant | pranzo |
| `profile_art_lover_car()` | CAR | museum, monument | pranzo |

**Effetti sul pipeline:**

- `DistanceMatrix` — usa `profile.travel_time_min(km)` per i tempi di percorrenza
- `GreedySeeder` — filtra `allowed_pois` per categoria; usa `effective_score()` (con boost tag)
- `RepairEngine` — filtra categorie, applica cap ristoranti/snack, garantisce slot pasto
- `FitnessEvaluator` — calcola score con boost da `tag_weights`; penalizza pasti mancanti

---

## 6. Funzione fitness multi-obiettivo

La fitness è calcolata in `FitnessEvaluator.evaluate()` e comprende tre componenti separate per NSGA-II e uno scalare aggregato per confronti rapidi (tournament selection):

### Score effettivo con boost

```python
effective_score(poi) = min(poi.score × Π(1 + tag_weight - 1), 1.5)
```

I `tag_weights` del profilo amplificano i PoI tematicamente rilevanti (es. `arte × 1.4` per un turista culturale). Il cap a 1.5 evita distorsioni eccessive.

### Scalare aggregato

```
scalar = w_score × norm(total_score)
       - w_dist  × norm(total_distance)
       - time_over_penalty     # solo sullo sforamento, non sull'uso del budget
       - wait_penalty          # penalizza attese cumulate > 5 min
       - meal_penalty          # penalizza slot pasto non coperti
```

La scelta deliberata di **non penalizzare l'uso del budget** (solo lo sforamento) evita che il GA produca tour brevissimi per minimizzare il tempo. Un tour di 10 ore che rispetta il budget di 11 ore non è peggio di uno da 3 ore.

### I tre obiettivi Pareto

| Obiettivo | Misura | Direzione |
|-----------|--------|-----------|
| `total_score` | score effettivo cumulato | Massimizza |
| `total_distance` | km percorsi (Haversine × 1.3) | Minimizza |
| `total_time` | minuti totali del tour | Minimizza |

La dominanza Pareto è implementata in `FitnessScore.dominates()`:

```python
def dominates(self, other) -> bool:
    better_or_equal = (score ≥ and dist ≤ and time ≤)
    strictly_better = (score > or dist < or time <)
    return better_or_equal and strictly_better
```

---

## 7. Operatori genetici

### Selezione

**Tournament selection** con `k=3` candidati casuali. Con `use_pareto=True` (default) preferisce rango Pareto basso e crowding distance alta, implementando l'operatore crowded-comparison di NSGA-II.

### Crossover

**Order Crossover (OX) adattato** — Preserva l'ordine relativo dei PoI condivisi tra i genitori senza duplicati. Adattato al TOP-TW per gestire cromosomi a lunghezza variabile (sottoinsiemi, non permutazioni complete).

```
Genitore A: [Trevi, Navona, Pantheon, Pranzo, Colosseo]
Genitore B: [Colosseo, Pranzo, Borghese, Trevi, Castel]

Segmento da A (pos 1-2): [Navona, Pantheon]
Riempimento da B (escl. duplicati): [Colosseo, Pranzo, Trevi, Castel]
Figlio: [Colosseo, Navona, Pantheon, Pranzo, Trevi, Castel]
```

**PoI-aware Crossover** — Scambia interi blocchi per categoria (es. tutti i musei da A, tutti i ristoranti da B). Preserva la "nicchia tematica" del genitore e mantiene la coerenza del profilo turista.

### Mutazione

Quattro operatori applicati con probabilità adattiva:

| Operatore | Effetto | Caso d'uso |
|-----------|---------|------------|
| `swap_mutation` | Scambia 2 PoI nella sequenza | Esplorazione locale |
| `insert_mutation` | Rimuove e reinserisce un PoI | Fix ordinamento temporale |
| `reverse_segment_mutation` | Inverte un sottosegmento | Elimina crossing geografici |
| `add_remove_mutation` | Aggiunge/rimuove un PoI dal pool ammesso | Modifica lunghezza tour |

La `add_remove_mutation` opera **solo sul pool filtrato** per le categorie del profilo — non può mai inserire un ristorante nel tour di un turista che li ha esclusi.

---

## 8. Riparazione genetica (Repair Engine)

Ogni individuo viene riparato dopo ogni operatore genetico, prima della valutazione fitness. La pipeline di riparazione ha **7 step in sequenza**:

```
1. _filter_allowed_categories   → rimuove PoI di categorie non ammesse
2. _sort_by_earliest_deadline   → riordina per orario di apertura (EDF)
3. repair_time_windows          → rimuove PoI con TW violata o attesa > max_wait_min
4. repair_budget                → rimuove PoI a minor score/durata finché budget rispettato
5. _cap_restaurants             → limita ristoranti formali al numero di slot pasto
6. _cap_snacks                  → limita bar e gelaterie ai massimi del profilo
7. _ensure_meal_slots           → garantisce un ristorante per ogni slot pasto richiesto
```

### Step 3: max_wait_min

Il parametro `max_wait_min` (default 30) definisce la massima attesa tollerata per qualsiasi PoI. Un ristorante che apre alle 12:00 e a cui si arriva alle 9:30 viene **rimosso** — non ha senso aspettare 2 ore e mezza. L'eccezione è `_ensure_meal_slots`, che tolera fino a 45 minuti di attesa per garantire la sosta pranzo/cena.

### Step 7: strategia di inserimento/sostituzione

Quando deve garantire un pasto, `_ensure_meal_slots` prova due strategie:

1. **Inserimento diretto** — aggiunge il ristorante nella posizione temporalmente corretta senza sforare il budget.
2. **Sostituzione** — se non c'è spazio, rimuove il PoI con il peggior rapporto score/durata e lo sostituisce con il ristorante. Questo evita che tour a budget pieno perdano la garanzia del pasto.

---

## 9. Greedy Seeding

La popolazione iniziale è costruita con una strategia mista 20/20/60:

| Quota | Strategia | Scopo |
|-------|-----------|-------|
| 20% | Greedy puro (`alpha=0`) | Massima qualità iniziale, convergenza rapida |
| 20% | α-greedy perturbato (`alpha` da 0.15 a 0.50) | Varietà controllata vicino all'ottimo |
| 60% | Casuale riparato | Massima diversità genetica |

### Criterio greedy: ratio score/overhead

```python
ratio = effective_score(poi) / (travel_min + wait_min + visit_duration)
```

Dove `effective_score` include già i boost da `tag_weights` del profilo. Il criterio seleziona il PoI che massimizza il valore per minuto di tempo investito (spostamento + attesa + visita).

### Restricted Candidate List (GRASP-like)

Con `alpha > 0`, invece di scegliere sempre il candidato migliore, si sceglie **casualmente tra il top 20%** per ratio. Questo produce soluzioni diverse ma ancora di buona qualità, avvicinandosi alla strategia GRASP (Greedy Randomized Adaptive Search Procedure).

Tutti gli individui iniziali (greedy inclusi) passano per `repair.repair()` per garantire la coerenza con i vincoli del profilo fin dalla prima generazione.

---

## 10. Modello di trasporto realistico

Il calcolo dei tempi di percorrenza in `profile.travel_time_min(km)` usa un modello che riflette la realtà urbana:

### WALK

```
t = km / v_walk
```
- `v_walk_normal = 4.5 km/h`, `v_walk_limited = 3.0 km/h`

### TRANSIT (bus + metro)

```
if km < 0.40:          # soglia: prendere il mezzo non conviene
    t = km / v_walk
else:
    t = km / 20.0 + 10 min   # 10 min overhead (attesa + fermata)
```

L'overhead fisso di 10 minuti per tratta modella la realtà di Roma: frequenza media bus 8-12 minuti, metro 4-5 minuti, più il cammino alle fermate.

### CAR / TAXI

```
t = km / 25.0 + 5 min   # 5 min overhead parcheggio
```

### MIXED

Per distanze sotto `600 m` si usa `v_walk`; oltre si usa la velocità transit con overhead.

---

## 11. Configurazione e avvio

### Requisiti

```
Python ≥ 3.10   (per syntax X | None nei type hint)
Nessuna dipendenza esterna (stdlib only)
```

### SolverConfig

```python
from tour_ga.solver import SolverConfig

config = SolverConfig(
    pop_size         = 80,      # dimensione popolazione
    max_generations  = 300,     # generazioni massime
    cx_prob          = 0.85,    # probabilità crossover
    mut_prob         = 0.20,    # probabilità mutazione
    tournament_k     = 3,       # candidati per torneo
    stagnation_limit = 50,      # early stop per stagnazione
    start_time       = 540,     # 09:00 (minuti dalla mezzanotte)
    budget           = 480,     # 8 ore di tour
    start_lat        = 41.896,  # coordinate punto di partenza
    start_lon        = 12.484,
    max_wait_min     = 30,      # attesa massima tollerata per PoI
    w_score          = 0.50,    # peso obiettivo score
    w_dist           = 0.20,    # peso obiettivo distanza
    w_time           = 0.30,    # peso penalità tempo
)
```

### Avvio base

```python
from tour_ga.core.models import PoI, PoICategory, TimeWindow
from tour_ga.core.distance import DistanceMatrix
from tour_ga.core.profile import profile_cultural_walker
from tour_ga.solver import NSGA2Solver, SolverConfig

pois = [...]   # lista di PoI
profile = profile_cultural_walker()

dm = DistanceMatrix(pois, profile=profile)
dm.build()     # calcola matrice distanze (una volta sola)

solver = NSGA2Solver(pois, dm, config, profile=profile)
pareto_front = solver.solve()  # restituisce lista di Individual

# Selezione della soluzione consigliata dal fronte
best = max(pareto_front, key=lambda x: x.fitness.scalar)
schedule = solver.evaluator.decode(best)
print(schedule.summary())
```

### Profilo custom

```python
from tour_ga.core.profile import TouristProfile, TransportMode

profile = TouristProfile(
    transport_mode     = TransportMode.TRANSIT,
    allowed_categories = ["monument", "viewpoint", "bar"],
    want_lunch         = False,
    want_dinner        = False,
    max_bar_stops      = 2,
    tag_weights        = {"fotogenico": 1.5, "panorama": 1.3},
)
```

### Callback di monitoraggio

```python
def progress(gen, pareto_front, stats):
    print(f"Gen {gen}: pareto={stats['pareto_size']}, "
          f"best={stats['best_scalar']:.4f}, "
          f"feasible={stats['feasible_pct']:.0f}%")

front = solver.solve(callback=progress)
```

### Demo completa

```bash
python tour_ga/demo_rome.py
```

---

## 12. Risultati di esempio

Configurazione: `budget=660 min` (11h), `start_time=570` (09:30), `pop_size=60`, `max_generations=200`, Roma.

### Profilo gastronomico con mezzi (TRANSIT, pranzo + cena)

```
09:42–10:12  Fontana di Trevi
10:24–10:54  Piazza di Spagna
11:08–11:53  Piazza Navona
11:56–12:16  Sant'Eustachio il Caffè         ← bar pomeridiano
12:27–13:27  Osteria del Rione               ← pranzo garantito
13:41–15:11  Castel Sant'Angelo
15:24–16:24  Pantheon
16:37–18:07  Foro Romano
18:21–18:41  Fatamorgana                     ← gelateria pomeridiana
19:00–20:20  Ristorante San Pietro           ← cena garantita (attesa 4 min)
Totale: 650 min, 10.9 km, attese 4 min
Composizione: 1×bar, 1×gelateria, 4×monument, 2×restaurant, 2×viewpoint
```

### Profilo culturale a piedi (WALK, solo pranzo)

```
09:39–10:09  Fontana di Trevi
10:18–10:48  Piazza di Spagna
11:06–11:51  Piazza Navona
12:00–13:00  Osteria del Rione               ← pranzo (attesa 3 min)
13:05–14:05  Pantheon
14:21–15:51  Foro Romano
16:01–18:01  Colosseo
18:32–20:02  Trastevere
Totale: 632 min, 8.1 km, attese 3 min
```

### Profilo custom: solo viste, nessun pasto

```
09:39–10:09  Fontana di Trevi
10:18–10:48  Piazza di Spagna
11:06–11:51  Piazza Navona
11:54–12:14  Sant'Eustachio il Caffè
12:16–13:16  Pantheon
13:33–15:03  Castel Sant'Angelo
15:14–15:34  Fatamorgana
Totale: 364 min, 5.5 km
```

---

## 13. Riferimenti

### Articolo scientifico principale

Deb, K., Pratap, A., Agarwal, S., & Meyarivan, T. (2002). **A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II**. *IEEE Transactions on Evolutionary Computation*, 6(2), 182–197.
[https://www.cse.unr.edu/~sushil/class/gas/papers/nsga2.pdf](https://www.cse.unr.edu/~sushil/class/gas/papers/nsga2.pdf)

Il paper introduce tre contributi fondamentali qui implementati: il fast non-dominated sorting O(MN²) tramite conteggio della dominanza (implementato in `solver.py::_fast_non_dominated_sort`), la crowding distance come meccanismo di diversità parameter-free (implementato in `_assign_crowding_distance`), e il crowded-comparison operator per selezione a torneo (in `operators.py::tournament_select`).

### Riferimento divulgativo

Non-Dominated Sorting Genetic Algorithm 2 (NSGA-II) — GeeksforGeeks.
[https://www.geeksforgeeks.org/deep-learning/non-dominated-sorting-genetic-algorithm-2-nsga-ii/](https://www.geeksforgeeks.org/deep-learning/non-dominated-sorting-genetic-algorithm-2-nsga-ii/)

### Problema di riferimento

Vansteenwegen, P., Souffriau, W., & Van Oudheusden, D. (2011). **The Orienteering Problem: A Survey**. *European Journal of Operational Research*, 209(1), 1–10.

Chao, I. M., Golden, B. L., & Wasil, E. A. (1996). **The Team Orienteering Problem**. *European Journal of Operational Research*, 88(3), 464–474.

### Calcolo distanze

Formula di Haversine per la distanza geodetica tra due coordinate GPS, con fattore correttivo 1.3 per approssimare il percorso urbano reale rispetto alla linea d'aria.