Spaces:
Sleeping
Sleeping
| """ | |
| 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" | |
| 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}" | |
| 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})" | |
| 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 | |
| class ScheduledStop: | |
| poi: PoI | |
| arrival: int # minuti dalla mezzanotte | |
| departure: int # minuti dalla mezzanotte | |
| wait: int # minuti di attesa prima dell'apertura | |
| 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})" |