Spaces:
Sleeping
Sleeping
File size: 8,259 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 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 | """
app.py — FastAPI backend for Tour Generator.
Exposes API endpoints for generating tours.
"""
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from pydantic import BaseModel
import json
from typing import List, Optional
from core.models import PoI, PoICategory, TimeWindow
from core.profile import (
TouristProfile, MobilityLevel, TransportMode,
profile_cultural_walker, profile_foodie_transit,
profile_family_mixed, profile_art_lover_car
)
from core.distance import DistanceMatrix, haversine_km
from solver import NSGA2Solver, SolverConfig
import pandas as pd
app = FastAPI(title="Tour Generator API", description="API for generating optimized tours using genetic algorithms")
# Predefined profiles
PREDEFINED_PROFILES = {
"cultural_walker": profile_cultural_walker(),
"foodie_transit": profile_foodie_transit(),
"family_mixed": profile_family_mixed(),
"art_lover_car": profile_art_lover_car(),
}
class PoIModel(BaseModel):
id: str
name: str
lat: float
lon: float
score: float
visit_duration: int
time_window_open: int
time_window_close: int
category: str
tags: List[str] = []
class ProfileModel(BaseModel):
transport_mode: TransportMode = TransportMode.WALK
mobility: MobilityLevel = MobilityLevel.NORMAL
allowed_categories: List[str] = ["museum", "monument", "restaurant", "park", "viewpoint"]
want_lunch: bool = True
want_dinner: bool = True
lunch_time: int = 720
dinner_time: int = 1140
meal_window: int = 120
max_bar_stops: int = 2
max_gelateria_stops: int = 1
tag_weights: dict = {}
max_entry_fee: Optional[float] = None
group_size: int = 1
@app.post("/generate_tour")
async def generate_tour(
pois_file: Optional[UploadFile] = File(None),
pois_json: Optional[str] = Form(None),
profile_name: Optional[str] = Form(None),
profile_json: Optional[str] = Form(None),
budget: int = Form(480),
start_time: int = Form(540),
start_lat: float = Form(41.9028),
start_lon: float = Form(12.4964),
):
"""
Generate an optimized tour based on POIs and user profile.
- pois_file: Upload CSV or JSON file with POIs
- pois_json: JSON string with list of POIs
- profile_name: Name of predefined profile
- profile_json: JSON string with custom profile
- budget: Time budget in minutes
- start_time: Start time in minutes from midnight
- start_lat/lon: Starting coordinates
"""
# Load POIs
pois = []
if pois_file:
content = await pois_file.read()
if pois_file.filename.endswith('.csv'):
df = pd.read_csv(pd.io.common.BytesIO(content))
for _, row in df.iterrows():
pois.append(PoI(
id=str(row['id']),
name=str(row['name']),
lat=float(row['lat']),
lon=float(row['lon']),
score=float(row['score']),
visit_duration=int(row['visit_duration']),
time_window=TimeWindow(int(row['time_window_open']), int(row['time_window_close'])),
category=PoICategory(row['category']),
tags=str(row.get('tags', '')).split(',') if pd.notna(row.get('tags')) else []
))
elif pois_file.filename.endswith('.json'):
data = json.loads(content.decode('utf-8'))
for p in data:
pois.append(PoI(
id=p['id'],
name=p['name'],
lat=p['lat'],
lon=p['lon'],
score=p['score'],
visit_duration=p['visit_duration'],
time_window=TimeWindow(p['time_window']['open'], p['time_window']['close']),
category=PoICategory(p['category']),
tags=p.get('tags', [])
))
else:
raise HTTPException(status_code=400, detail="Unsupported file type for POIs. Use CSV or JSON.")
elif pois_json:
pois_data = json.loads(pois_json)
for p in pois_data:
pois.append(PoI(
id=p['id'],
name=p['name'],
lat=p['lat'],
lon=p['lon'],
score=p['score'],
visit_duration=p['visit_duration'],
time_window=TimeWindow(p['time_window']['open'], p['time_window']['close']),
category=PoICategory(p['category']),
tags=p.get('tags', [])
))
else:
raise HTTPException(status_code=400, detail="POIs not provided. Upload a file or provide JSON.")
# Load profile
if profile_name:
if profile_name in PREDEFINED_PROFILES:
profile = PREDEFINED_PROFILES[profile_name]
else:
raise HTTPException(status_code=400, detail=f"Invalid profile name. Available: {list(PREDEFINED_PROFILES.keys())}")
elif profile_json:
profile_data = json.loads(profile_json)
profile = TouristProfile(**profile_data)
else:
profile = TouristProfile() # default
# Create distance matrix
dm = DistanceMatrix(pois, profile)
# Config
config = SolverConfig(budget=budget, start_time=start_time, start_lat=start_lat, start_lon=start_lon)
# Solve
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}%")
solver = NSGA2Solver(pois, dm, config, profile)
population = solver.solve(callback=cb)
feasible = [x for x in population if x.fitness.is_feasible] or population
if not feasible:
raise HTTPException(status_code=500, detail="No solutions found")
# Get best tour (highest scalar fitness)
best = max(feasible, key=lambda individual: individual.fitness.scalar)
tour = solver.evaluator.decode(best)
if tour is None:
raise HTTPException(status_code=500, detail="Failed to generate schedule")
# Return as dict
stops_list = []
for i, s in enumerate(tour.stops):
if i == 0:
dist = haversine_km(start_lat, start_lon, s.poi.lat, s.poi.lon)
else:
dist = haversine_km(tour.stops[i-1].poi.lat, tour.stops[i-1].poi.lon, s.poi.lat, s.poi.lon)
time_min = profile.travel_time_min(dist)
stop_dict = {
"poi_id": s.poi.id,
"poi_name": s.poi.name,
"arrival": s.arrival,
"departure": s.departure,
"wait": s.wait,
"travel_distance_km": round(dist, 2),
"travel_time_min": time_min
}
stops_list.append(stop_dict)
return {
"total_score": best.fitness.total_score,
"total_distance": tour.total_distance,
"total_time": tour.total_time,
"is_feasible": tour.is_feasible,
"stops": stops_list
}
@app.get("/profiles")
def get_profiles():
"""Get list of available predefined profiles."""
return {"profiles": list(PREDEFINED_PROFILES.keys())}
@app.get("/profiles/{name}")
def get_profile(name: str):
"""Get details of a specific predefined profile."""
if name in PREDEFINED_PROFILES:
profile = PREDEFINED_PROFILES[name]
return {
"transport_mode": profile.transport_mode.value,
"mobility": profile.mobility.value,
"allowed_categories": profile.allowed_categories,
"want_lunch": profile.want_lunch,
"want_dinner": profile.want_dinner,
"lunch_time": profile.lunch_time,
"dinner_time": profile.dinner_time,
"meal_window": profile.meal_window,
"max_bar_stops": profile.max_bar_stops,
"max_gelateria_stops": profile.max_gelateria_stops,
"tag_weights": profile.tag_weights,
"max_entry_fee": profile.max_entry_fee,
"group_size": profile.group_size
}
else:
raise HTTPException(status_code=404, detail="Profile not found")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) |