File size: 4,497 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
"""
core/models.py — Strutture dati fondamentali per il TOP-TW turistico.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import math


class PoICategory(Enum):
    MUSEUM     = "museum"
    MONUMENT   = "monument"
    RESTAURANT = "restaurant"   # pranzo / cena formale
    BAR        = "bar"          # caffè, aperitivo, sosta breve
    GELATERIA  = "gelateria"    # sosta dolce pomeridiana
    PARK       = "park"
    VIEWPOINT  = "viewpoint"


@dataclass
class TimeWindow:
    open:  int   # minuti dalla mezzanotte (es. 540 = 09:00)
    close: int   # minuti dalla mezzanotte (es. 1080 = 18:00)

    def __repr__(self) -> str:
        return f"{self.open//60:02d}:{self.open%60:02d}{self.close//60:02d}:{self.close%60:02d}"


@dataclass
class PoI:
    id:             str
    name:           str
    lat:            float
    lon:            float
    score:          float          # interesse normalizzato [0, 1]
    visit_duration: int            # minuti di visita stimati
    time_window:    TimeWindow
    category:       PoICategory
    tags:           list[str]      = field(default_factory=list)

    def __hash__(self):
        return hash(self.id)

    def __eq__(self, other):
        return isinstance(other, PoI) and self.id == other.id

    def __repr__(self):
        return f"PoI({self.name!r}, score={self.score:.2f}, {self.time_window})"


@dataclass
class FitnessScore:
    total_score:    float = 0.0   # somma score PoI visitati
    total_distance: float = 0.0   # km totali percorsi
    total_time:     int   = 0     # minuti totali (spostamenti + visite)
    is_feasible:    bool  = False  # rispetta TW e budget?
    scalar:         float = 0.0   # valore aggregato per confronti rapidi
    rank:           int   = 0     # rango Pareto (NSGA-II)
    crowd:          float = 0.0   # crowding distance (NSGA-II)

    def dominates(self, other: FitnessScore) -> bool:
        """
        self domina other se è ≥ su tutti gli obiettivi e > su almeno uno.
        Obiettivi: massimizza score, minimizza distance, minimizza time.
        """
        better_or_equal = (
            self.total_score    >= other.total_score and
            self.total_distance <= other.total_distance and
            self.total_time     <= other.total_time
        )
        strictly_better = (
            self.total_score    > other.total_score or
            self.total_distance < other.total_distance or
            self.total_time     < other.total_time
        )
        return better_or_equal and strictly_better


@dataclass
class ScheduledStop:
    poi:         PoI
    arrival:     int   # minuti dalla mezzanotte
    departure:   int   # minuti dalla mezzanotte
    wait:        int   # minuti di attesa prima dell'apertura


@dataclass
class TourSchedule:
    stops:          list[ScheduledStop] = field(default_factory=list)
    total_time:     int   = 0
    total_distance: float = 0.0
    total_wait:     int   = 0    # minuti di attesa cumulati (attese a TW)
    is_feasible:    bool  = False

    def summary(self) -> str:
        lines = []
        for s in self.stops:
            a = f"{s.arrival//60:02d}:{s.arrival%60:02d}"
            d = f"{s.departure//60:02d}:{s.departure%60:02d}"
            w = f" (attesa {s.wait} min)" if s.wait > 0 else ""
            lines.append(f"  {a}{d}  {s.poi.name}{w}")
        wait_note = f", attese {self.total_wait} min" if self.total_wait > 0 else ""
        lines.append(
            f"  Totale: {self.total_time} min, "
            f"{self.total_distance:.1f} km{wait_note}"
        )
        return "\n".join(lines)


class Individual:
    """
    Cromosoma = lista ordinata di PoI che compongono il tour.
    Il gene jolly (WildcardGene) è un placeholder che viene
    materializzato al momento della decodifica.
    """

    def __init__(self, genes: list[PoI]):
        self.genes:    list[PoI]        = genes
        self.fitness:  FitnessScore     = FitnessScore()
        self._schedule: Optional[TourSchedule] = None  # cache

    def clone(self) -> Individual:
        return Individual(genes=list(self.genes))

    def invalidate_cache(self):
        self._schedule = None
        self.fitness   = FitnessScore()

    def __len__(self):
        return len(self.genes)

    def __repr__(self):
        names = [p.name for p in self.genes]
        return f"Individual([{', '.join(names)}], scalar={self.fitness.scalar:.3f})"