Tour_Generator_GA / core /models.py
GaetanoParente's picture
first commit
639f871
"""
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})"