cleaned up backend to use classes and yaml files
This commit is contained in:
parent
14a7f555df
commit
94fa735d54
@ -1,128 +1,104 @@
|
|||||||
import math as m
|
import math as m
|
||||||
import json, os
|
import yaml
|
||||||
|
import logging
|
||||||
|
|
||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple
|
||||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||||
|
from OSMPythonTools import cachingStrategy
|
||||||
from pywikibot import ItemPage, Site
|
from pywikibot import ItemPage, Site
|
||||||
from pywikibot import config
|
from pywikibot import config
|
||||||
config.put_throttle = 0
|
config.put_throttle = 0
|
||||||
config.maxlag = 0
|
config.maxlag = 0
|
||||||
|
|
||||||
from structs.landmarks import Landmark, LandmarkType
|
|
||||||
from structs.preferences import Preferences, Preference
|
from structs.preferences import Preferences, Preference
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
from utils import take_most_important
|
||||||
|
import constants
|
||||||
|
|
||||||
|
|
||||||
SIGHTSEEING = LandmarkType(landmark_type='sightseeing')
|
SIGHTSEEING = 'sightseeing'
|
||||||
NATURE = LandmarkType(landmark_type='nature')
|
NATURE = 'nature'
|
||||||
SHOPPING = LandmarkType(landmark_type='shopping')
|
SHOPPING = 'shopping'
|
||||||
|
|
||||||
|
|
||||||
# Include the json here
|
|
||||||
# Create a list of all things to visit given some preferences and a city. Ready for the optimizer
|
|
||||||
def generate_landmarks(preferences: Preferences, coordinates: Tuple[float, float]) :
|
|
||||||
|
|
||||||
l_sights, l_nature, l_shop = get_amenities()
|
class LandmarkManager:
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
city_bbox_side: int # bbox side in meters
|
||||||
|
radius_close_to: int # radius in meters
|
||||||
|
church_coeff: float # coeff to adjsut score of churches
|
||||||
|
park_coeff: float # coeff to adjust score of parks
|
||||||
|
tag_coeff: float # coeff to adjust weight of tags
|
||||||
|
N_important: int # number of important landmarks to consider
|
||||||
|
|
||||||
|
preferences: Preferences # preferences of visit
|
||||||
|
location: Tuple[float] # coordinates around which to find a path
|
||||||
|
|
||||||
|
def __init__(self, preferences: Preferences, coordinates: Tuple[float, float]) -> None:
|
||||||
|
|
||||||
|
with constants.AMENITY_SELECTORS_PATH.open('r') as f:
|
||||||
|
self.amenity_selectors = yaml.safe_load(f)
|
||||||
|
|
||||||
|
with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
|
||||||
|
parameters = yaml.safe_load(f)
|
||||||
|
self.city_bbox_side = parameters['city_bbox_side']
|
||||||
|
self.radius_close_to = parameters['radius_close_to']
|
||||||
|
self.church_coeff = parameters['church_coeff']
|
||||||
|
self.park_coeff = parameters['park_coeff']
|
||||||
|
self.tag_coeff = parameters['tag_coeff']
|
||||||
|
self.N_important = parameters['N_important']
|
||||||
|
|
||||||
|
self.preferences = preferences
|
||||||
|
self.location = coordinates
|
||||||
|
|
||||||
|
|
||||||
|
def generate_landmarks_list(self) -> Tuple[List[Landmark], List[Landmark]] :
|
||||||
|
"""
|
||||||
|
Generate and prioritize a list of landmarks based on user preferences.
|
||||||
|
|
||||||
|
This method fetches landmarks from various categories (sightseeing, nature, shopping) based on the user's preferences
|
||||||
|
and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
|
||||||
|
landmarks based on a predefined criterion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[Landmark], List[Landmark]]:
|
||||||
|
- A list of all existing landmarks.
|
||||||
|
- A list of the most important landmarks based on the user's preferences.
|
||||||
|
"""
|
||||||
|
|
||||||
L = []
|
L = []
|
||||||
|
|
||||||
# List for sightseeing
|
# List for sightseeing
|
||||||
if preferences.sightseeing.score != 0 :
|
if self.preferences.sightseeing.score != 0 :
|
||||||
L1 = get_landmarks(l_sights, SIGHTSEEING, coordinates=coordinates)
|
L1 = self.fetch_landmarks(self.amenity_selectors['sightseeing'], SIGHTSEEING, coordinates=self.location)
|
||||||
correct_score(L1, preferences.sightseeing)
|
self.correct_score(L1, self.preferences.sightseeing)
|
||||||
L += L1
|
L += L1
|
||||||
|
|
||||||
# List for nature
|
# List for nature
|
||||||
if preferences.nature.score != 0 :
|
if self.preferences.nature.score != 0 :
|
||||||
L2 = get_landmarks(l_nature, NATURE, coordinates=coordinates)
|
L2 = self.fetch_landmarks(self.amenity_selectors['nature'], NATURE, coordinates=self.location)
|
||||||
correct_score(L2, preferences.nature)
|
self.correct_score(L2, self.preferences.nature)
|
||||||
L += L2
|
L += L2
|
||||||
|
|
||||||
# List for shopping
|
# List for shopping
|
||||||
if preferences.shopping.score != 0 :
|
if self.preferences.shopping.score != 0 :
|
||||||
L3 = get_landmarks(l_shop, SHOPPING, coordinates=coordinates)
|
L3 = self.fetch_landmarks(self.amenity_selectors['shopping'], SHOPPING, coordinates=self.location)
|
||||||
correct_score(L3, preferences.shopping)
|
self.correct_score(L3, self.preferences.shopping)
|
||||||
L += L3
|
L += L3
|
||||||
|
|
||||||
L = remove_duplicates(L)
|
L = self.remove_duplicates(L)
|
||||||
|
|
||||||
return L, take_most_important(L)
|
return L, take_most_important(L, self.N_important)
|
||||||
|
|
||||||
|
|
||||||
# Helper function to gather the amenities list
|
def remove_duplicates(self, landmarks: List[Landmark]) -> List[Landmark] :
|
||||||
def get_amenities() -> List[List[str]] :
|
|
||||||
|
|
||||||
# Get the list of amenities from the files
|
|
||||||
sightseeing = get_list('/amenities/sightseeing.am')
|
|
||||||
nature = get_list('/amenities/nature.am')
|
|
||||||
shopping = get_list('/amenities/shopping.am')
|
|
||||||
|
|
||||||
return sightseeing, nature, shopping
|
|
||||||
|
|
||||||
|
|
||||||
# Helper function to read a .am file and generate the corresponding list
|
|
||||||
def get_list(path: str) -> List[str] :
|
|
||||||
|
|
||||||
with open(os.path.dirname(os.path.abspath(__file__)) + path) as f :
|
|
||||||
content = f.readlines()
|
|
||||||
|
|
||||||
amenities = []
|
|
||||||
for line in content :
|
|
||||||
if not line.startswith('#') :
|
|
||||||
amenities.append(line.strip('\n'))
|
|
||||||
|
|
||||||
return amenities
|
|
||||||
|
|
||||||
|
|
||||||
# Take the most important landmarks from the list
|
|
||||||
def take_most_important(L: List[Landmark], N = 0) -> List[Landmark] :
|
|
||||||
|
|
||||||
# Read the parameters from the file
|
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
|
|
||||||
parameters = json.loads(f.read())
|
|
||||||
N_important = parameters['N important']
|
|
||||||
|
|
||||||
L_copy = []
|
|
||||||
L_clean = []
|
|
||||||
scores = [0]*len(L)
|
|
||||||
names = []
|
|
||||||
name_id = {}
|
|
||||||
|
|
||||||
for i, elem in enumerate(L) :
|
|
||||||
if elem.name not in names :
|
|
||||||
names.append(elem.name)
|
|
||||||
name_id[elem.name] = [i]
|
|
||||||
L_copy.append(elem)
|
|
||||||
else :
|
|
||||||
name_id[elem.name] += [i]
|
|
||||||
scores = []
|
|
||||||
for j in name_id[elem.name] :
|
|
||||||
scores.append(L[j].attractiveness)
|
|
||||||
best_id = max(range(len(scores)), key=scores.__getitem__)
|
|
||||||
t = name_id[elem.name][best_id]
|
|
||||||
if t == i :
|
|
||||||
for old in L_copy :
|
|
||||||
if old.name == elem.name :
|
|
||||||
old.attractiveness = L[t].attractiveness
|
|
||||||
|
|
||||||
scores = [0]*len(L_copy)
|
|
||||||
for i, elem in enumerate(L_copy) :
|
|
||||||
scores[i] = elem.attractiveness
|
|
||||||
|
|
||||||
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-N):]
|
|
||||||
|
|
||||||
for i, elem in enumerate(L_copy) :
|
|
||||||
if i in res :
|
|
||||||
L_clean.append(elem)
|
|
||||||
|
|
||||||
return L_clean
|
|
||||||
|
|
||||||
|
|
||||||
# Remove duplicate elements and elements with low score
|
|
||||||
def remove_duplicates(L: List[Landmark]) -> List[Landmark] :
|
|
||||||
"""
|
"""
|
||||||
Removes duplicate landmarks based on their names from the given list.
|
Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
L (List[Landmark]): A list of Landmark objects.
|
landmarks (List[Landmark]): A list of Landmark objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Landmark]: A list of unique Landmark objects based on their names.
|
List[Landmark]: A list of unique Landmark objects based on their names.
|
||||||
@ -131,11 +107,9 @@ def remove_duplicates(L: List[Landmark]) -> List[Landmark] :
|
|||||||
L_clean = []
|
L_clean = []
|
||||||
names = []
|
names = []
|
||||||
|
|
||||||
for landmark in L :
|
for landmark in landmarks :
|
||||||
if landmark.name in names:
|
if landmark.name in names:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
else :
|
else :
|
||||||
names.append(landmark.name)
|
names.append(landmark.name)
|
||||||
L_clean.append(landmark)
|
L_clean.append(landmark)
|
||||||
@ -143,25 +117,51 @@ def remove_duplicates(L: List[Landmark]) -> List[Landmark] :
|
|||||||
return L_clean
|
return L_clean
|
||||||
|
|
||||||
|
|
||||||
# Correct the score of a list of landmarks by taking into account preference settings
|
def correct_score(self, landmarks: List[Landmark], preference: Preference) :
|
||||||
def correct_score(L: List[Landmark], preference: Preference) :
|
"""
|
||||||
|
Adjust the attractiveness score of each landmark in the list based on user preferences.
|
||||||
|
|
||||||
if len(L) == 0 :
|
This method updates the attractiveness of each landmark by scaling it according to the user's preference score.
|
||||||
|
The score adjustment is computed using a simple linear transformation based on the preference score.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmarks (List[Landmark]): A list of landmarks whose scores need to be corrected.
|
||||||
|
preference (Preference): The user's preference settings that influence the attractiveness score adjustment.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If the type of any landmark in the list does not match the expected type in the preference.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(landmarks) == 0 :
|
||||||
return
|
return
|
||||||
|
|
||||||
if L[0].type != preference.type :
|
if landmarks[0].type != preference.type :
|
||||||
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
|
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}")
|
||||||
|
|
||||||
for elem in L :
|
for elem in landmarks :
|
||||||
elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation
|
elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation
|
||||||
|
|
||||||
|
|
||||||
# Function to count elements within a certain radius of a location
|
def count_elements_close_to(self, coordinates: Tuple[float, float]) -> int:
|
||||||
def count_elements_within_radius(coordinates: Tuple[float, float], radius: int) -> int:
|
"""
|
||||||
|
Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.
|
||||||
|
|
||||||
|
This function constructs a bounding box around the specified coordinates based on the radius. It then queries
|
||||||
|
OpenStreetMap data to count the number of elements within that bounding box.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coordinates (Tuple[float, float]): The latitude and longitude of the location to search around.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
|
||||||
|
are found or if an error occurs during the query.
|
||||||
|
"""
|
||||||
|
|
||||||
lat = coordinates[0]
|
lat = coordinates[0]
|
||||||
lon = coordinates[1]
|
lon = coordinates[1]
|
||||||
|
|
||||||
|
radius = self.radius_close_to
|
||||||
|
|
||||||
alpha = (180*radius) / (6371000*m.pi)
|
alpha = (180*radius) / (6371000*m.pi)
|
||||||
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
|
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
|
||||||
|
|
||||||
@ -177,19 +177,27 @@ def count_elements_within_radius(coordinates: Tuple[float, float], radius: int)
|
|||||||
if N_elem is None :
|
if N_elem is None :
|
||||||
return 0
|
return 0
|
||||||
return N_elem
|
return N_elem
|
||||||
|
|
||||||
except :
|
except :
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
# Creates a bounding box around given coordinates, side_length in meters
|
def create_bbox(self, coordinates: Tuple[float, float]) -> Tuple[float, float, float, float]:
|
||||||
def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[float, float, float, float]:
|
"""
|
||||||
|
Create a bounding box around the given coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coordinates (Tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
|
||||||
|
defining the bounding box.
|
||||||
|
"""
|
||||||
|
|
||||||
lat = coordinates[0]
|
lat = coordinates[0]
|
||||||
lon = coordinates[1]
|
lon = coordinates[1]
|
||||||
|
|
||||||
# Half the side length in km (since it's a square bbox)
|
# Half the side length in km (since it's a square bbox)
|
||||||
half_side_length_km = side_length / 2 / 1000
|
half_side_length_km = self.city_bbox_side / 2 / 1000
|
||||||
|
|
||||||
# Convert distance to degrees
|
# Convert distance to degrees
|
||||||
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
|
lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km
|
||||||
@ -204,19 +212,28 @@ def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[flo
|
|||||||
return min_lat, min_lon, max_lat, max_lon
|
return min_lat, min_lon, max_lat, max_lon
|
||||||
|
|
||||||
|
|
||||||
def get_landmarks(list_amenity: list, landmarktype: LandmarkType, coordinates: Tuple[float, float]) -> List[Landmark] :
|
def fetch_landmarks(self, list_amenity: list, landmarktype: str, coordinates: Tuple[float, float]) -> List[Landmark] :
|
||||||
|
"""
|
||||||
|
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
|
||||||
|
|
||||||
# Read the parameters from the file
|
Args:
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
|
list_amenity (list): A list of OSM amenity queries to be used for fetching landmarks.
|
||||||
parameters = json.loads(f.read())
|
These queries are typically used to filter results (e.g., [''amenity'='place_of_worship']).
|
||||||
tag_coeff = parameters['tag coeff']
|
landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping').
|
||||||
park_coeff = parameters['park coeff']
|
coordinates (Tuple[float, float]): The central coordinates (latitude, longitude) for the bounding box.
|
||||||
church_coeff = parameters['church coeff']
|
|
||||||
radius = parameters['radius close to']
|
Returns:
|
||||||
bbox_side = parameters['city bbox side']
|
List[Landmark]: A list of Landmark objects that were fetched and filtered based on the provided criteria.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The bounding box is created around the given coordinates with a side length defined by `self.city_bbox_side`.
|
||||||
|
- Landmarks are fetched using Overpass API queries.
|
||||||
|
- Landmarks are filtered based on various conditions including tags and type.
|
||||||
|
- Scores are assigned to landmarks based on their attributes and surrounding elements.
|
||||||
|
"""
|
||||||
|
|
||||||
# Create bbox around start location
|
# Create bbox around start location
|
||||||
bbox = create_bbox(coordinates, bbox_side)
|
bbox = self.create_bbox(coordinates)
|
||||||
|
|
||||||
# Initialize some variables
|
# Initialize some variables
|
||||||
N = 0
|
N = 0
|
||||||
@ -272,9 +289,9 @@ def get_landmarks(list_amenity: list, landmarktype: LandmarkType, coordinates: T
|
|||||||
n_languages = len(item.labels)
|
n_languages = len(item.labels)
|
||||||
n_tags += n_languages/10
|
n_tags += n_languages/10
|
||||||
|
|
||||||
if elem_type != LandmarkType(landmark_type="nature") :
|
if elem_type != "nature" :
|
||||||
if "leisure" in tag and elem.tag('leisure') == "park":
|
if "leisure" in tag and elem.tag('leisure') == "park":
|
||||||
elem_type = LandmarkType(landmark_type="nature")
|
elem_type = "nature"
|
||||||
|
|
||||||
if amenity not in ["'shop'='department_store'", "'shop'='mall'"] :
|
if amenity not in ["'shop'='department_store'", "'shop'='mall'"] :
|
||||||
if "shop" in tag :
|
if "shop" in tag :
|
||||||
@ -290,14 +307,15 @@ def get_landmarks(list_amenity: list, landmarktype: LandmarkType, coordinates: T
|
|||||||
|
|
||||||
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
|
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
|
||||||
if amenity == "'amenity'='place_of_worship'" :
|
if amenity == "'amenity'='place_of_worship'" :
|
||||||
#score = int((count_elements_within_radius(location, radius) + (n_tags*tag_coeff) )*church_coeff)
|
#score = int((count_elements_close_to(location, radius) + (n_tags*tag_coeff) )*church_coeff)
|
||||||
score = int((count_elements_within_radius(location, radius) + ((n_tags**1.2)*tag_coeff) )*church_coeff)
|
score = int((self.count_elements_close_to(location) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)
|
||||||
elif amenity == "'leisure'='park'" :
|
elif amenity == "'leisure'='park'" :
|
||||||
score = int((count_elements_within_radius(location, radius) + ((n_tags**1.2)*tag_coeff) )*park_coeff)
|
score = int((self.count_elements_close_to(location) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)
|
||||||
else :
|
else :
|
||||||
score = int(count_elements_within_radius(location, radius) + ((n_tags**1.2)*tag_coeff))
|
score = int(self.count_elements_close_to(location) + ((n_tags**1.2)*self.tag_coeff))
|
||||||
|
|
||||||
if score is not None :
|
if score is not None :
|
||||||
|
|
||||||
# Generate the landmark and append it to the list
|
# Generate the landmark and append it to the list
|
||||||
#print(f"There are {n_tags} tags on this Landmark. Total score : {score}\n")
|
#print(f"There are {n_tags} tags on this Landmark. Total score : {score}\n")
|
||||||
landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=int(n_tags))
|
landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=int(n_tags))
|
||||||
|
570
backend/src/optimizer.py
Normal file
570
backend/src/optimizer.py
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
import yaml, logging
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
from scipy.optimize import linprog
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from geopy.distance import geodesic
|
||||||
|
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
import constants
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Optimizer:
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
landmarks: List[Landmark] # list of landmarks
|
||||||
|
max_time: int = None # max visit time (in minutes)
|
||||||
|
detour: int = None # accepted max detour time (in minutes)
|
||||||
|
detour_factor: float # detour factor of straight line vs real distance in cities
|
||||||
|
average_walking_speed: float # average walking speed of adult
|
||||||
|
max_landmarks: int # max number of landmarks to visit
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, max_time: int, landmarks: List[Landmark]) :
|
||||||
|
self.max_time = max_time
|
||||||
|
self.landmarks = landmarks
|
||||||
|
|
||||||
|
# load parameters from file
|
||||||
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||||
|
parameters = yaml.safe_load(f)
|
||||||
|
self.detour_factor = parameters['detour_factor']
|
||||||
|
self.average_walking_speed = parameters['average_walking_speed']
|
||||||
|
self.max_landmarks = parameters['max_landmarks']
|
||||||
|
|
||||||
|
|
||||||
|
def print_res(self, L: List[Landmark]):
|
||||||
|
"""
|
||||||
|
Print the suggested order of landmarks to visit and log the total time walked.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (List[Landmark]): List of landmarks in the suggested visit order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.logger.info(f'The following order is suggested : ')
|
||||||
|
dist = 0
|
||||||
|
for elem in L :
|
||||||
|
if elem.time_to_reach_next is not None :
|
||||||
|
print('- ' + elem.name + ', time to reach next = ' + str(elem.time_to_reach_next))
|
||||||
|
dist += elem.time_to_reach_next
|
||||||
|
else :
|
||||||
|
print('- ' + elem.name)
|
||||||
|
self.logger.info(f'Minutes walked : {dist}')
|
||||||
|
self.logger.info(f'Visited {len(L)-2} landmarks')
|
||||||
|
|
||||||
|
|
||||||
|
# Prevent the use of a particular solution
|
||||||
|
def prevent_config(self, resx):
|
||||||
|
"""
|
||||||
|
Prevent the use of a particular solution by adding constraints to the optimization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resx (List[float]): List of edge weights.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[int], List[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for i, elem in enumerate(resx):
|
||||||
|
resx[i] = round(elem)
|
||||||
|
|
||||||
|
N = len(resx) # Number of edges
|
||||||
|
L = int(np.sqrt(N)) # Number of landmarks
|
||||||
|
|
||||||
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funky so I use the [0]
|
||||||
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
|
ind_a = nonzero_tup[0].tolist()
|
||||||
|
vertices_visited = ind_a
|
||||||
|
vertices_visited.remove(0)
|
||||||
|
|
||||||
|
ones = [1]*L
|
||||||
|
h = [0]*N
|
||||||
|
for i in range(L) :
|
||||||
|
if i in vertices_visited :
|
||||||
|
h[i*L:i*L+L] = ones
|
||||||
|
|
||||||
|
return h, [len(vertices_visited)-1]
|
||||||
|
|
||||||
|
|
||||||
|
# Prevents the creation of the same circle (both directions)
|
||||||
|
def prevent_circle(self, circle_vertices: list, L: int) :
|
||||||
|
"""
|
||||||
|
Prevent circular paths by by adding constraints to the optimization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
circle_vertices (list): List of vertices forming a circle.
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||||
|
"""
|
||||||
|
|
||||||
|
l1 = [0]*L*L
|
||||||
|
l2 = [0]*L*L
|
||||||
|
for i, node in enumerate(circle_vertices[:-1]) :
|
||||||
|
next = circle_vertices[i+1]
|
||||||
|
|
||||||
|
l1[node*L + next] = 1
|
||||||
|
l2[next*L + node] = 1
|
||||||
|
|
||||||
|
s = circle_vertices[0]
|
||||||
|
g = circle_vertices[-1]
|
||||||
|
|
||||||
|
l1[g*L + s] = 1
|
||||||
|
l2[s*L + g] = 1
|
||||||
|
|
||||||
|
return np.vstack((l1, l2)), [0, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def is_connected(self, resx) :
|
||||||
|
"""
|
||||||
|
Determine the order of visits and detect any circular paths in the given configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resx (list): List of edge weights.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[int], Optional[List[List[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# first round the results to have only 0-1 values
|
||||||
|
for i, elem in enumerate(resx):
|
||||||
|
resx[i] = round(elem)
|
||||||
|
|
||||||
|
N = len(resx) # length of res
|
||||||
|
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||||
|
|
||||||
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||||
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
|
ind_a = nonzero_tup[0].tolist()
|
||||||
|
ind_b = nonzero_tup[1].tolist()
|
||||||
|
|
||||||
|
# Step 1: Create a graph representation
|
||||||
|
graph = defaultdict(list)
|
||||||
|
for a, b in zip(ind_a, ind_b):
|
||||||
|
graph[a].append(b)
|
||||||
|
|
||||||
|
# Step 2: Function to perform BFS/DFS to extract journeys
|
||||||
|
def get_journey(start):
|
||||||
|
journey_nodes = []
|
||||||
|
visited = set()
|
||||||
|
stack = deque([start])
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
if node not in visited:
|
||||||
|
visited.add(node)
|
||||||
|
journey_nodes.append(node)
|
||||||
|
for neighbor in graph[node]:
|
||||||
|
if neighbor not in visited:
|
||||||
|
stack.append(neighbor)
|
||||||
|
|
||||||
|
return journey_nodes
|
||||||
|
|
||||||
|
# Step 3: Extract all journeys
|
||||||
|
all_journeys_nodes = []
|
||||||
|
visited_nodes = set()
|
||||||
|
|
||||||
|
for node in ind_a:
|
||||||
|
if node not in visited_nodes:
|
||||||
|
journey_nodes = get_journey(node)
|
||||||
|
all_journeys_nodes.append(journey_nodes)
|
||||||
|
visited_nodes.update(journey_nodes)
|
||||||
|
|
||||||
|
for l in all_journeys_nodes :
|
||||||
|
if 0 in l :
|
||||||
|
order = l
|
||||||
|
all_journeys_nodes.remove(l)
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(all_journeys_nodes) == 0 :
|
||||||
|
return order, None
|
||||||
|
|
||||||
|
return order, all_journeys_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def get_time(self, p1: Tuple[float, float], p2: Tuple[float, float]) -> int :
|
||||||
|
"""
|
||||||
|
Calculate the time in minutes to travel from one location to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
p1 (Tuple[float, float]): Coordinates of the starting location.
|
||||||
|
p2 (Tuple[float, float]): Coordinates of the destination.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Time to travel from p1 to p2 in minutes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compute the straight-line distance in km
|
||||||
|
if p1 == p2 :
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
dist = geodesic(p1, p2).kilometers
|
||||||
|
|
||||||
|
# Consider the detour factor for average cityto deterline walking distance (in km)
|
||||||
|
walk_dist = dist*self.detour_factor
|
||||||
|
|
||||||
|
# Time to walk this distance (in minutes)
|
||||||
|
walk_time = walk_dist/self.average_walking_speed*60
|
||||||
|
|
||||||
|
return round(walk_time)
|
||||||
|
|
||||||
|
|
||||||
|
def init_ub_dist(self, landmarks: List[Landmark], max_steps: int):
|
||||||
|
"""
|
||||||
|
Initialize the objective function coefficients and inequality constraints for the optimization problem.
|
||||||
|
|
||||||
|
This function computes the distances between all landmarks and stores their attractiveness to maximize sightseeing.
|
||||||
|
The goal is to maximize the objective function subject to the constraints A*x < b and A_eq*x = b_eq.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmarks (List[Landmark]): List of landmarks.
|
||||||
|
max_steps (int): Maximum number of steps allowed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[float], List[float], List[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
||||||
|
c = []
|
||||||
|
# Coefficients of inequality constraints (left-hand side)
|
||||||
|
A_ub = []
|
||||||
|
|
||||||
|
for spot1 in landmarks :
|
||||||
|
dist_table = [0]*len(landmarks)
|
||||||
|
c.append(-spot1.attractiveness)
|
||||||
|
for j, spot2 in enumerate(landmarks) :
|
||||||
|
t = self.get_time(spot1.location, spot2.location)
|
||||||
|
dist_table[j] = t
|
||||||
|
closest = sorted(dist_table)[:22]
|
||||||
|
for i, dist in enumerate(dist_table) :
|
||||||
|
if dist not in closest :
|
||||||
|
dist_table[i] = 32700
|
||||||
|
A_ub += dist_table
|
||||||
|
c = c*len(landmarks)
|
||||||
|
|
||||||
|
return c, A_ub, [max_steps]
|
||||||
|
|
||||||
|
|
||||||
|
def respect_number(self, L: int):
|
||||||
|
"""
|
||||||
|
Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ones = [1]*L
|
||||||
|
zeros = [0]*L
|
||||||
|
A = ones + zeros*(L-1)
|
||||||
|
b = [1]
|
||||||
|
for i in range(L-1) :
|
||||||
|
h_new = zeros*i + ones + zeros*(L-1-i)
|
||||||
|
A = np.vstack((A, h_new))
|
||||||
|
b.append(1)
|
||||||
|
|
||||||
|
A = np.vstack((A, ones*L))
|
||||||
|
b.append(self.max_landmarks+1)
|
||||||
|
|
||||||
|
return A, b
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to not have d14 and d41 simultaneously. Does not prevent cyclic paths with more elements
|
||||||
|
def break_sym(self, L):
|
||||||
|
"""
|
||||||
|
Generate constraints to prevent simultaneous travel between two landmarks in both directions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
upper_ind = np.triu_indices(L,0,L)
|
||||||
|
|
||||||
|
up_ind_x = upper_ind[0]
|
||||||
|
up_ind_y = upper_ind[1]
|
||||||
|
|
||||||
|
A = [0]*L*L
|
||||||
|
b = [1]
|
||||||
|
|
||||||
|
for i, _ in enumerate(up_ind_x[1:]) :
|
||||||
|
l = [0]*L*L
|
||||||
|
if up_ind_x[i] != up_ind_y[i] :
|
||||||
|
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
||||||
|
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
||||||
|
|
||||||
|
A = np.vstack((A,l))
|
||||||
|
b.append(1)
|
||||||
|
|
||||||
|
return A, b
|
||||||
|
|
||||||
|
|
||||||
|
def init_eq_not_stay(self, L: int):
|
||||||
|
"""
|
||||||
|
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[np.ndarray], List[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
l = [0]*L*L
|
||||||
|
|
||||||
|
for i in range(L) :
|
||||||
|
for j in range(L) :
|
||||||
|
if j == i :
|
||||||
|
l[j + i*L] = 1
|
||||||
|
|
||||||
|
l = np.array(np.array(l), dtype=np.int8)
|
||||||
|
|
||||||
|
return [l], [0]
|
||||||
|
|
||||||
|
|
||||||
|
def respect_user_must_do(self, landmarks: List[Landmark]) :
|
||||||
|
"""
|
||||||
|
Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmarks (List[Landmark]): List of landmarks, where some are marked as 'must_do'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
L = len(landmarks)
|
||||||
|
A = [0]*L*L
|
||||||
|
b = [0]
|
||||||
|
|
||||||
|
for i, elem in enumerate(landmarks[1:]) :
|
||||||
|
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
||||||
|
l = [0]*L*L
|
||||||
|
l[i*L:i*L+L] = [1]*L # set mandatory departures from landmarks tagged as 'must_do'
|
||||||
|
|
||||||
|
A = np.vstack((A,l))
|
||||||
|
b.append(1)
|
||||||
|
|
||||||
|
return A, b
|
||||||
|
|
||||||
|
|
||||||
|
def respect_user_must_avoid(self, landmarks: List[Landmark]) :
|
||||||
|
"""
|
||||||
|
Generate constraints to ensure that landmarks marked as 'must_avoid' are skipped in the optimization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmarks (List[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
L = len(landmarks)
|
||||||
|
A = [0]*L*L
|
||||||
|
b = [0]
|
||||||
|
|
||||||
|
for i, elem in enumerate(landmarks[1:]) :
|
||||||
|
if elem.must_avoid is True and elem.name not in ['finish', 'start']:
|
||||||
|
l = [0]*L*L
|
||||||
|
l[i*L:i*L+L] = [1]*L
|
||||||
|
|
||||||
|
A = np.vstack((A,l))
|
||||||
|
b.append(0) # prevent departures from landmarks tagged as 'must_do'
|
||||||
|
|
||||||
|
return A, b
|
||||||
|
|
||||||
|
|
||||||
|
# Constraint to ensure start at start and finish at goal
|
||||||
|
def respect_start_finish(self, L: int):
|
||||||
|
"""
|
||||||
|
Generate constraints to ensure that the optimization starts at the designated start landmark and finishes at the goal landmark.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones)
|
||||||
|
l_start[L-1] = 0 # prevents the jump from start to finish
|
||||||
|
l_goal = [0]*L*L # sets arrivals only for finish (vertical ones)
|
||||||
|
l_L = [0]*L*(L-1) + [1]*L # prevents arrivals at start and departures from goal
|
||||||
|
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
||||||
|
l_L[k*L] = 1
|
||||||
|
if k != 0 :
|
||||||
|
l_goal[k*L+L-1] = 1
|
||||||
|
|
||||||
|
A = np.vstack((l_start, l_goal))
|
||||||
|
b = [1, 1]
|
||||||
|
A = np.vstack((A,l_L))
|
||||||
|
b.append(0)
|
||||||
|
|
||||||
|
return A, b
|
||||||
|
|
||||||
|
|
||||||
|
def respect_order(self, L: int):
|
||||||
|
"""
|
||||||
|
Generate constraints to tie the optimization problem together and prevent stacked ones, although this does not fully prevent circles.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[np.ndarray, List[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
A = [0]*L*L
|
||||||
|
b = [0]
|
||||||
|
for i in range(L-1) : # Prevent stacked ones
|
||||||
|
if i == 0 or i == L-1: # Don't touch start or finish
|
||||||
|
continue
|
||||||
|
else :
|
||||||
|
l = [0]*L
|
||||||
|
l[i] = -1
|
||||||
|
l = l*L
|
||||||
|
for j in range(L) :
|
||||||
|
l[i*L + j] = 1
|
||||||
|
|
||||||
|
A = np.vstack((A,l))
|
||||||
|
b.append(0)
|
||||||
|
|
||||||
|
return A, b
|
||||||
|
|
||||||
|
|
||||||
|
def link_list(self, order: List[int], landmarks: List[Landmark])->List[Landmark] :
|
||||||
|
"""
|
||||||
|
Compute the time to reach from each landmark to the next and create a list of landmarks with updated travel times.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order (List[int]): List of indices representing the order of landmarks to visit.
|
||||||
|
landmarks (List[Landmark]): List of all landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Landmark]]: The updated linked list of landmarks with travel times
|
||||||
|
"""
|
||||||
|
|
||||||
|
L = []
|
||||||
|
j = 0
|
||||||
|
while j < len(order)-1 :
|
||||||
|
# get landmarks involved
|
||||||
|
elem = landmarks[order[j]]
|
||||||
|
next = landmarks[order[j+1]]
|
||||||
|
|
||||||
|
# get attributes
|
||||||
|
elem.time_to_reach_next = self.get_time(elem.location, next.location)
|
||||||
|
elem.must_do = True
|
||||||
|
elem.location = (round(elem.location[0], 5), round(elem.location[1], 5))
|
||||||
|
elem.next_uuid = next.uuid
|
||||||
|
L.append(elem)
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
next.location = (round(next.location[0], 5), round(next.location[1], 5))
|
||||||
|
next.must_do = True
|
||||||
|
L.append(next)
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
# Main optimization pipeline
|
||||||
|
def solve_optimization (self) :
|
||||||
|
"""
|
||||||
|
Main optimization pipeline to solve the landmark visiting problem.
|
||||||
|
|
||||||
|
This method sets up and solves a linear programming problem with constraints to find an optimal tour of landmarks,
|
||||||
|
considering user-defined must-visit landmarks, start and finish points, and ensuring no cycles are present.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
L = len(self.landmarks)
|
||||||
|
|
||||||
|
# SET CONSTRAINTS FOR INEQUALITY
|
||||||
|
c, A_ub, b_ub = self.init_ub_dist(self.landmarks, self.max_time) # Add the distances from each landmark to the other
|
||||||
|
A, b = self.respect_number(L) # Respect max number of visits (no more possible stops than landmarks).
|
||||||
|
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
||||||
|
b_ub += b
|
||||||
|
A, b = self.break_sym(L) # break the 'zig-zag' symmetry
|
||||||
|
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
||||||
|
b_ub += b
|
||||||
|
|
||||||
|
|
||||||
|
# SET CONSTRAINTS FOR EQUALITY
|
||||||
|
A_eq, b_eq = self.init_eq_not_stay(L) # Force solution not to stay in same place
|
||||||
|
A, b = self.respect_user_must_do(self.landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||||
|
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
|
b_eq += b
|
||||||
|
A, b = self.respect_start_finish(L) # Force start and finish positions
|
||||||
|
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
|
b_eq += b
|
||||||
|
A, b = self.respect_order(L) # Respect order of visit (only works when max_steps is limiting factor)
|
||||||
|
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
|
b_eq += b
|
||||||
|
|
||||||
|
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
||||||
|
x_bounds = [(0, 1)]*L*L
|
||||||
|
|
||||||
|
# Solve linear programming problem
|
||||||
|
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||||
|
|
||||||
|
# Raise error if no solution is found
|
||||||
|
if not res.success :
|
||||||
|
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
||||||
|
|
||||||
|
# If there is a solution, we're good to go, just check for connectiveness
|
||||||
|
else :
|
||||||
|
order, circles = self.is_connected(res.x)
|
||||||
|
#nodes, edges = is_connected(res.x)
|
||||||
|
i = 0
|
||||||
|
timeout = 80
|
||||||
|
while circles is not None and i < timeout:
|
||||||
|
A, b = self.prevent_config(res.x)
|
||||||
|
A_ub = np.vstack((A_ub, A))
|
||||||
|
b_ub += b
|
||||||
|
#A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub)
|
||||||
|
for circle in circles :
|
||||||
|
A, b = self.prevent_circle(circle, L)
|
||||||
|
A_eq = np.vstack((A_eq, A))
|
||||||
|
b_eq += b
|
||||||
|
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
||||||
|
if not res.success :
|
||||||
|
self.logger.info("Solving failed because of overconstrained problem")
|
||||||
|
return None
|
||||||
|
order, circles = self.is_connected(res.x)
|
||||||
|
#nodes, edges = is_connected(res.x)
|
||||||
|
if circles is None :
|
||||||
|
break
|
||||||
|
print(i)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if i == timeout :
|
||||||
|
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||||
|
|
||||||
|
# Add the times to reach and stop optimizing
|
||||||
|
tour = self.link_list(order, self.landmarks)
|
||||||
|
|
||||||
|
# logging
|
||||||
|
if i != 0 :
|
||||||
|
self.logger.info(f"Neded to recompute paths {i} times because of unconnected loops...")
|
||||||
|
self.print_res(tour) # how to do better ?
|
||||||
|
self.logger.info(f"Total score : {int(-res.fun)}")
|
||||||
|
|
||||||
|
return tour
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,446 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
import json, os
|
|
||||||
|
|
||||||
from typing import List, Tuple
|
|
||||||
from scipy.optimize import linprog
|
|
||||||
from collections import defaultdict, deque
|
|
||||||
from geopy.distance import geodesic
|
|
||||||
|
|
||||||
from structs.landmarks import Landmark
|
|
||||||
|
|
||||||
|
|
||||||
# Function to print the result
|
|
||||||
def print_res(L: List[Landmark]):
|
|
||||||
|
|
||||||
print('The following order is suggested : ')
|
|
||||||
|
|
||||||
dist = 0
|
|
||||||
for elem in L :
|
|
||||||
if elem.time_to_reach_next is not None :
|
|
||||||
print('- ' + elem.name + ', time to reach next = ' + str(elem.time_to_reach_next))
|
|
||||||
dist += elem.time_to_reach_next
|
|
||||||
else :
|
|
||||||
print('- ' + elem.name)
|
|
||||||
|
|
||||||
print("\nMinutes walked : " + str(dist))
|
|
||||||
print(f"Visited {len(L)-2} landmarks")
|
|
||||||
|
|
||||||
|
|
||||||
# Prevent the use of a particular solution
|
|
||||||
def prevent_config(resx):
|
|
||||||
|
|
||||||
for i, elem in enumerate(resx):
|
|
||||||
resx[i] = round(elem)
|
|
||||||
|
|
||||||
N = len(resx) # Number of edges
|
|
||||||
L = int(np.sqrt(N)) # Number of landmarks
|
|
||||||
|
|
||||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funky so I use the [0]
|
|
||||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
|
||||||
|
|
||||||
ind_a = nonzero_tup[0].tolist()
|
|
||||||
vertices_visited = ind_a
|
|
||||||
vertices_visited.remove(0)
|
|
||||||
|
|
||||||
ones = [1]*L
|
|
||||||
h = [0]*N
|
|
||||||
for i in range(L) :
|
|
||||||
if i in vertices_visited :
|
|
||||||
h[i*L:i*L+L] = ones
|
|
||||||
|
|
||||||
return h, [len(vertices_visited)-1]
|
|
||||||
|
|
||||||
|
|
||||||
# Prevents the creation of the same circle (both directions)
|
|
||||||
def prevent_circle(circle_vertices: list, L: int) :
|
|
||||||
|
|
||||||
l1 = [0]*L*L
|
|
||||||
l2 = [0]*L*L
|
|
||||||
for i, node in enumerate(circle_vertices[:-1]) :
|
|
||||||
next = circle_vertices[i+1]
|
|
||||||
|
|
||||||
l1[node*L + next] = 1
|
|
||||||
l2[next*L + node] = 1
|
|
||||||
|
|
||||||
s = circle_vertices[0]
|
|
||||||
g = circle_vertices[-1]
|
|
||||||
|
|
||||||
l1[g*L + s] = 1
|
|
||||||
l2[s*L + g] = 1
|
|
||||||
|
|
||||||
return np.vstack((l1, l2)), [0, 0]
|
|
||||||
|
|
||||||
|
|
||||||
# Returns the order of visit as well as any circles if there are some
|
|
||||||
def is_connected(resx) :
|
|
||||||
|
|
||||||
# first round the results to have only 0-1 values
|
|
||||||
for i, elem in enumerate(resx):
|
|
||||||
resx[i] = round(elem)
|
|
||||||
|
|
||||||
N = len(resx) # length of res
|
|
||||||
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
|
||||||
n_edges = resx.sum() # number of edges
|
|
||||||
|
|
||||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
|
||||||
|
|
||||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
|
||||||
|
|
||||||
ind_a = nonzero_tup[0].tolist()
|
|
||||||
ind_b = nonzero_tup[1].tolist()
|
|
||||||
|
|
||||||
# Step 1: Create a graph representation
|
|
||||||
graph = defaultdict(list)
|
|
||||||
for a, b in zip(ind_a, ind_b):
|
|
||||||
graph[a].append(b)
|
|
||||||
|
|
||||||
# Step 2: Function to perform BFS/DFS to extract journeys
|
|
||||||
def get_journey(start):
|
|
||||||
journey_nodes = []
|
|
||||||
visited = set()
|
|
||||||
stack = deque([start])
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
node = stack.pop()
|
|
||||||
if node not in visited:
|
|
||||||
visited.add(node)
|
|
||||||
journey_nodes.append(node)
|
|
||||||
for neighbor in graph[node]:
|
|
||||||
if neighbor not in visited:
|
|
||||||
stack.append(neighbor)
|
|
||||||
|
|
||||||
return journey_nodes
|
|
||||||
|
|
||||||
# Step 3: Extract all journeys
|
|
||||||
all_journeys_nodes = []
|
|
||||||
visited_nodes = set()
|
|
||||||
|
|
||||||
for node in ind_a:
|
|
||||||
if node not in visited_nodes:
|
|
||||||
journey_nodes = get_journey(node)
|
|
||||||
all_journeys_nodes.append(journey_nodes)
|
|
||||||
visited_nodes.update(journey_nodes)
|
|
||||||
|
|
||||||
for l in all_journeys_nodes :
|
|
||||||
if 0 in l :
|
|
||||||
order = l
|
|
||||||
all_journeys_nodes.remove(l)
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(all_journeys_nodes) == 0 :
|
|
||||||
return order, None
|
|
||||||
|
|
||||||
return order, all_journeys_nodes
|
|
||||||
|
|
||||||
|
|
||||||
# Function that returns the time in minutes from one location to another
|
|
||||||
def get_time(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: float) :
|
|
||||||
|
|
||||||
# Compute the straight-line distance in km
|
|
||||||
if p1 == p2 :
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
dist = geodesic(p1, p2).kilometers
|
|
||||||
|
|
||||||
# Consider the detour factor for average cityto deterline walking distance (in km)
|
|
||||||
walk_dist = dist*detour
|
|
||||||
|
|
||||||
# Time to walk this distance (in minutes)
|
|
||||||
walk_time = walk_dist/speed*60
|
|
||||||
|
|
||||||
return round(walk_time)
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
|
|
||||||
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
|
|
||||||
def init_ub_dist(landmarks: List[Landmark], max_steps: int):
|
|
||||||
|
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
|
||||||
parameters = json.loads(f.read())
|
|
||||||
detour = parameters['detour factor']
|
|
||||||
speed = parameters['average walking speed']
|
|
||||||
|
|
||||||
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
|
||||||
c = []
|
|
||||||
# Coefficients of inequality constraints (left-hand side)
|
|
||||||
A_ub = []
|
|
||||||
|
|
||||||
for spot1 in landmarks :
|
|
||||||
dist_table = [0]*len(landmarks)
|
|
||||||
c.append(-spot1.attractiveness)
|
|
||||||
for j, spot2 in enumerate(landmarks) :
|
|
||||||
t = get_time(spot1.location, spot2.location, detour, speed)
|
|
||||||
dist_table[j] = t
|
|
||||||
closest = sorted(dist_table)[:22]
|
|
||||||
for i, dist in enumerate(dist_table) :
|
|
||||||
if dist not in closest :
|
|
||||||
dist_table[i] = 32700
|
|
||||||
A_ub += dist_table
|
|
||||||
c = c*len(landmarks)
|
|
||||||
|
|
||||||
return c, A_ub, [max_steps]
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to respect only one travel per landmark. Also caps the total number of visited landmarks
|
|
||||||
def respect_number(L: int, max_landmarks: int):
|
|
||||||
|
|
||||||
ones = [1]*L
|
|
||||||
zeros = [0]*L
|
|
||||||
A = ones + zeros*(L-1)
|
|
||||||
b = [1]
|
|
||||||
for i in range(L-1) :
|
|
||||||
h_new = zeros*i + ones + zeros*(L-1-i)
|
|
||||||
A = np.vstack((A, h_new))
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
if max_landmarks is None :
|
|
||||||
# Read the parameters from the file
|
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
|
||||||
parameters = json.loads(f.read())
|
|
||||||
max_landmarks = parameters['max landmarks']
|
|
||||||
|
|
||||||
A = np.vstack((A, ones*L))
|
|
||||||
b.append(max_landmarks+1)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to not have d14 and d41 simultaneously. Does not prevent cyclic paths with more elements
|
|
||||||
def break_sym(L):
|
|
||||||
|
|
||||||
upper_ind = np.triu_indices(L,0,L)
|
|
||||||
|
|
||||||
up_ind_x = upper_ind[0]
|
|
||||||
up_ind_y = upper_ind[1]
|
|
||||||
|
|
||||||
A = [0]*L*L # useless row to prevent overhead ? better solution welcomed
|
|
||||||
# A[up_ind_x[0]*L + up_ind_y[0]] = 1
|
|
||||||
# A[up_ind_y[0]*L + up_ind_x[0]] = 1
|
|
||||||
b = [1]
|
|
||||||
|
|
||||||
for i, _ in enumerate(up_ind_x[1:]) :
|
|
||||||
l = [0]*L*L
|
|
||||||
if up_ind_x[i] != up_ind_y[i] :
|
|
||||||
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
|
||||||
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
|
||||||
|
|
||||||
A = np.vstack((A,l))
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to not stay in position. Removes d11, d22, d33, etc.
|
|
||||||
def init_eq_not_stay(L: int):
|
|
||||||
l = [0]*L*L
|
|
||||||
|
|
||||||
for i in range(L) :
|
|
||||||
for j in range(L) :
|
|
||||||
if j == i :
|
|
||||||
l[j + i*L] = 1
|
|
||||||
|
|
||||||
l = np.array(np.array(l), dtype=np.int8)
|
|
||||||
|
|
||||||
return [l], [0]
|
|
||||||
|
|
||||||
|
|
||||||
# Go through the landmarks and force the optimizer to use landmarks marked as must_do
|
|
||||||
def respect_user_must_do(landmarks: List[Landmark]) :
|
|
||||||
L = len(landmarks)
|
|
||||||
|
|
||||||
A = [0]*L*L
|
|
||||||
b = [0]
|
|
||||||
for i, elem in enumerate(landmarks[1:]) :
|
|
||||||
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
|
||||||
l = [0]*L*L
|
|
||||||
l[i*L:i*L+L] = [1]*L # set mandatory departures from landmarks tagged as 'must_do'
|
|
||||||
|
|
||||||
A = np.vstack((A,l))
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to ensure start at start and finish at goal
|
|
||||||
def respect_start_finish(L: int):
|
|
||||||
l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones)
|
|
||||||
l_start[L-1] = 0 # prevents the jump from start to finish
|
|
||||||
l_goal = [0]*L*L # sets arrivals only for finish (vertical ones)
|
|
||||||
l_L = [0]*L*(L-1) + [1]*L # prevents arrivals at start and departures from goal
|
|
||||||
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
|
||||||
l_L[k*L] = 1
|
|
||||||
if k != 0 :
|
|
||||||
l_goal[k*L+L-1] = 1
|
|
||||||
|
|
||||||
|
|
||||||
A = np.vstack((l_start, l_goal))
|
|
||||||
b = [1, 1]
|
|
||||||
A = np.vstack((A,l_L))
|
|
||||||
b.append(0)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
|
|
||||||
def respect_order(L: int):
|
|
||||||
|
|
||||||
A = [0]*L*L # useless row to reduce overhead ? better solution is welcome
|
|
||||||
b = [0]
|
|
||||||
for i in range(L-1) : # Prevent stacked ones
|
|
||||||
if i == 0 or i == L-1: # Don't touch start or finish
|
|
||||||
continue
|
|
||||||
else :
|
|
||||||
l = [0]*L
|
|
||||||
l[i] = -1
|
|
||||||
l = l*L
|
|
||||||
for j in range(L) :
|
|
||||||
l[i*L + j] = 1
|
|
||||||
|
|
||||||
A = np.vstack((A,l))
|
|
||||||
b.append(0)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Computes the time to reach from each landmark to the next
|
|
||||||
def link_list(order: List[int], landmarks: List[Landmark])->List[Landmark] :
|
|
||||||
|
|
||||||
# Read the parameters from the file
|
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
|
||||||
parameters = json.loads(f.read())
|
|
||||||
detour_factor = parameters['detour factor']
|
|
||||||
speed = parameters['average walking speed']
|
|
||||||
|
|
||||||
L = []
|
|
||||||
j = 0
|
|
||||||
total_dist = 0
|
|
||||||
while j < len(order)-1 :
|
|
||||||
elem = landmarks[order[j]]
|
|
||||||
next = landmarks[order[j+1]]
|
|
||||||
|
|
||||||
d = get_time(elem.location, next.location, detour_factor, speed)
|
|
||||||
elem.time_to_reach_next = d
|
|
||||||
elem.must_do = True
|
|
||||||
elem.location = (round(elem.location[0], 5), round(elem.location[1], 5))
|
|
||||||
L.append(elem)
|
|
||||||
j += 1
|
|
||||||
total_dist += d
|
|
||||||
|
|
||||||
next.location = (round(next.location[0], 5), round(next.location[1], 5))
|
|
||||||
L.append(next)
|
|
||||||
|
|
||||||
return L, total_dist
|
|
||||||
|
|
||||||
|
|
||||||
# Same as link_list but does it on a already ordered list
|
|
||||||
def link_list_simple(ordered_visit: List[Landmark])-> List[Landmark] :
|
|
||||||
|
|
||||||
# Read the parameters from the file
|
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
|
||||||
parameters = json.loads(f.read())
|
|
||||||
detour_factor = parameters['detour factor']
|
|
||||||
speed = parameters['average walking speed']
|
|
||||||
|
|
||||||
L = []
|
|
||||||
j = 0
|
|
||||||
total_dist = 0
|
|
||||||
while j < len(ordered_visit)-1 :
|
|
||||||
elem = ordered_visit[j]
|
|
||||||
next = ordered_visit[j+1]
|
|
||||||
|
|
||||||
elem.next_uuid = next.uuid
|
|
||||||
d = get_time(elem.location, next.location, detour_factor, speed)
|
|
||||||
elem.time_to_reach_next = d
|
|
||||||
if elem.name not in ['start', 'finish'] :
|
|
||||||
elem.must_do = True
|
|
||||||
L.append(elem)
|
|
||||||
j += 1
|
|
||||||
total_dist += d
|
|
||||||
|
|
||||||
L.append(next)
|
|
||||||
|
|
||||||
return L, total_dist
|
|
||||||
|
|
||||||
|
|
||||||
# Main optimization pipeline
|
|
||||||
def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_details: bool, max_landmarks = None) :
|
|
||||||
|
|
||||||
L = len(landmarks)
|
|
||||||
|
|
||||||
# SET CONSTRAINTS FOR INEQUALITY
|
|
||||||
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
|
|
||||||
A, b = respect_number(L, max_landmarks) # Respect max number of visits (no more possible stops than landmarks).
|
|
||||||
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
|
||||||
b_ub += b
|
|
||||||
A, b = break_sym(L) # break the 'zig-zag' symmetry
|
|
||||||
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
|
||||||
b_ub += b
|
|
||||||
|
|
||||||
|
|
||||||
# SET CONSTRAINTS FOR EQUALITY
|
|
||||||
A_eq, b_eq = init_eq_not_stay(L) # Force solution not to stay in same place
|
|
||||||
A, b = respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
|
||||||
b_eq += b
|
|
||||||
A, b = respect_start_finish(L) # Force start and finish positions
|
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
|
||||||
b_eq += b
|
|
||||||
A, b = respect_order(L) # Respect order of visit (only works when max_steps is limiting factor)
|
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
|
||||||
b_eq += b
|
|
||||||
|
|
||||||
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
|
||||||
x_bounds = [(0, 1)]*L*L
|
|
||||||
|
|
||||||
# Solve linear programming problem
|
|
||||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
|
||||||
|
|
||||||
# Raise error if no solution is found
|
|
||||||
if not res.success :
|
|
||||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
|
||||||
|
|
||||||
# If there is a solution, we're good to go, just check for connectiveness
|
|
||||||
else :
|
|
||||||
order, circles = is_connected(res.x)
|
|
||||||
#nodes, edges = is_connected(res.x)
|
|
||||||
i = 0
|
|
||||||
timeout = 80
|
|
||||||
while circles is not None and i < timeout:
|
|
||||||
A, b = prevent_config(res.x)
|
|
||||||
A_ub = np.vstack((A_ub, A))
|
|
||||||
b_ub += b
|
|
||||||
#A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub)
|
|
||||||
for circle in circles :
|
|
||||||
A, b = prevent_circle(circle, len(landmarks))
|
|
||||||
A_eq = np.vstack((A_eq, A))
|
|
||||||
b_eq += b
|
|
||||||
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
|
|
||||||
if not res.success :
|
|
||||||
print("Solving failed because of overconstrained problem")
|
|
||||||
return None
|
|
||||||
order, circles = is_connected(res.x)
|
|
||||||
#nodes, edges = is_connected(res.x)
|
|
||||||
if circles is None :
|
|
||||||
break
|
|
||||||
print(i)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if i == timeout :
|
|
||||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
|
||||||
|
|
||||||
# Add the times to reach and stop optimizing
|
|
||||||
L, _ = link_list(order, landmarks)
|
|
||||||
|
|
||||||
if printing_details is True :
|
|
||||||
if i != 0 :
|
|
||||||
print(f"Neded to recompute paths {i} times because of unconnected loops...")
|
|
||||||
print_res(L)
|
|
||||||
print("\nTotal score : " + str(int(-res.fun)))
|
|
||||||
|
|
||||||
return L
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
city_bbox_side: 5000 #m
|
city_bbox_side: 5000 #m
|
||||||
radius_close_to: 30
|
radius_close_to: 50
|
||||||
church_coeff: 0.6
|
church_coeff: 0.8
|
||||||
park_coeff: 1.5
|
park_coeff: 1.2
|
||||||
tag_coeff: 100
|
tag_coeff: 10
|
||||||
N_important: 40
|
N_important: 40
|
||||||
|
@ -1,45 +1,110 @@
|
|||||||
import os, json
|
import yaml, logging
|
||||||
|
|
||||||
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
from math import pi
|
from math import pi
|
||||||
|
|
||||||
from structs.landmarks import Landmark
|
from structs.landmarks import Landmark
|
||||||
from landmarks_manager import take_most_important
|
from optimizer import Optimizer
|
||||||
from optimizer_v4 import solve_optimization, link_list_simple, print_res, get_time
|
from utils import get_time, link_list_simple, take_most_important
|
||||||
|
import constants
|
||||||
|
|
||||||
|
|
||||||
# Create corridor from tour
|
|
||||||
def create_corridor(landmarks: List[Landmark], width: float) :
|
class Refiner :
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
all_landmarks: List[Landmark] # list of all landmarks
|
||||||
|
base_tour: List[Landmark] # base tour that needs to be refined
|
||||||
|
max_time: int = None # max visit time (in minutes)
|
||||||
|
detour: int = None # accepted max detour time (in minutes)
|
||||||
|
detour_factor: float # detour factor of straight line vs real distance in cities
|
||||||
|
average_walking_speed: float # average walking speed of adult
|
||||||
|
max_landmarks: int # max number of landmarks to visit
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, max_time: int, detour: int, all_landmarks: List[Landmark], base_tour: List[Landmark]) :
|
||||||
|
self.max_time = max_time
|
||||||
|
self.detour = detour
|
||||||
|
self.all_landmarks = all_landmarks
|
||||||
|
self.base_tour = base_tour
|
||||||
|
|
||||||
|
# load parameters from file
|
||||||
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||||
|
parameters = yaml.safe_load(f)
|
||||||
|
self.detour_factor = parameters['detour_factor']
|
||||||
|
self.average_walking_speed = parameters['average_walking_speed']
|
||||||
|
self.max_landmarks = parameters['max_landmarks'] + 4
|
||||||
|
|
||||||
|
|
||||||
|
def create_corridor(self, landmarks: List[Landmark], width: float) :
|
||||||
|
"""
|
||||||
|
Create a corridor around the path connecting the landmarks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
landmarks (List[Landmark]): the landmark path around which to create the corridor
|
||||||
|
width (float): Width of the corridor in meters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Geometry: A buffered geometry object representing the corridor around the path.
|
||||||
|
"""
|
||||||
|
|
||||||
corrected_width = (180*width)/(6371000*pi)
|
corrected_width = (180*width)/(6371000*pi)
|
||||||
|
|
||||||
path = create_linestring(landmarks)
|
path = self.create_linestring(landmarks)
|
||||||
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
|
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
# Create linestring from tour
|
def create_linestring(self, tour: List[Landmark])->LineString :
|
||||||
def create_linestring(landmarks: List[Landmark])->List[Point] :
|
"""
|
||||||
|
Create a `LineString` object from a tour.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tour (List[Landmark]): An ordered sequence of landmarks that represents the visiting order.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LineString: A `LineString` object representing the path through the landmarks.
|
||||||
|
"""
|
||||||
|
|
||||||
points = []
|
points = []
|
||||||
|
for landmark in tour :
|
||||||
for landmark in landmarks :
|
|
||||||
points.append(Point(landmark.location))
|
points.append(Point(landmark.location))
|
||||||
|
|
||||||
return LineString(points)
|
return LineString(points)
|
||||||
|
|
||||||
|
|
||||||
# Check if some coordinates are in area. Used for the corridor
|
# Check if some coordinates are in area. Used for the corridor
|
||||||
def is_in_area(area: Polygon, coordinates) -> bool :
|
def is_in_area(self, area: Polygon, coordinates) -> bool :
|
||||||
|
"""
|
||||||
|
Check if a given point is within a specified area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
area (Polygon): The polygon defining the area.
|
||||||
|
coordinates (Tuple[float, float]): The coordinates of the point to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the point is within the area, otherwise False.
|
||||||
|
"""
|
||||||
point = Point(coordinates)
|
point = Point(coordinates)
|
||||||
return point.within(area)
|
return point.within(area)
|
||||||
|
|
||||||
|
|
||||||
# Function to determine if two landmarks are close to each other
|
# Function to determine if two landmarks are close to each other
|
||||||
def is_close_to(location1: Tuple[float], location2: Tuple[float]):
|
def is_close_to(self, location1: Tuple[float], location2: Tuple[float]):
|
||||||
"""Determine if two locations are close by rounding their coordinates to 3 decimals."""
|
"""
|
||||||
|
Determine if two locations are close to each other by rounding their coordinates to 3 decimal places.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location1 (Tuple[float, float]): The coordinates of the first location.
|
||||||
|
location2 (Tuple[float, float]): The coordinates of the second location.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the locations are within 0.001 degrees of each other, otherwise False.
|
||||||
|
"""
|
||||||
|
|
||||||
absx = abs(location1[0] - location2[0])
|
absx = abs(location1[0] - location2[0])
|
||||||
absy = abs(location1[1] - location2[1])
|
absy = abs(location1[1] - location2[1])
|
||||||
|
|
||||||
@ -47,36 +112,55 @@ def is_close_to(location1: Tuple[float], location2: Tuple[float]):
|
|||||||
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
|
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
|
||||||
|
|
||||||
|
|
||||||
# Rearrange some landmarks in the order of visit to group visit
|
def rearrange(self, tour: List[Landmark]) -> List[Landmark]:
|
||||||
def rearrange(landmarks: List[Landmark]) -> List[Landmark]:
|
"""
|
||||||
|
Rearrange landmarks to group nearby visits together.
|
||||||
|
|
||||||
|
This function reorders landmarks so that nearby landmarks are adjacent to each other in the list,
|
||||||
|
while keeping 'start' and 'finish' landmarks in their original positions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tour (List[Landmark]): Ordered list of landmarks to be rearranged.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Landmark]: The rearranged list of landmarks with grouped nearby visits.
|
||||||
|
"""
|
||||||
|
|
||||||
i = 1
|
i = 1
|
||||||
while i < len(landmarks):
|
while i < len(tour):
|
||||||
j = i+1
|
j = i+1
|
||||||
while j < len(landmarks):
|
while j < len(tour):
|
||||||
if is_close_to(landmarks[i].location, landmarks[j].location) and landmarks[i].name not in ['start', 'finish'] and landmarks[j].name not in ['start', 'finish']:
|
if self.is_close_to(tour[i].location, tour[j].location) and tour[i].name not in ['start', 'finish'] and tour[j].name not in ['start', 'finish']:
|
||||||
# If they are not adjacent, move the j-th element to be adjacent to the i-th element
|
# If they are not adjacent, move the j-th element to be adjacent to the i-th element
|
||||||
if j != i + 1:
|
if j != i + 1:
|
||||||
landmarks.insert(i + 1, landmarks.pop(j))
|
tour.insert(i + 1, tour.pop(j))
|
||||||
break # Move to the next i-th element after rearrangement
|
break # Move to the next i-th element after rearrangement
|
||||||
j += 1
|
j += 1
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return landmarks
|
return tour
|
||||||
|
|
||||||
|
|
||||||
# Simple nearest neighbour planner to try to fix the path
|
def find_shortest_path_through_all_landmarks(self, landmarks: List[Landmark]) -> Tuple[List[Landmark], Polygon]:
|
||||||
def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> Tuple[List[Landmark], Polygon]:
|
"""
|
||||||
|
Find the shortest path through all landmarks using a nearest neighbor heuristic.
|
||||||
|
|
||||||
# Read from data
|
This function constructs a path that starts from the 'start' landmark, visits all other landmarks in the order
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
of their proximity, and ends at the 'finish' landmark. It returns both the ordered list of landmarks and a
|
||||||
parameters = json.loads(f.read())
|
polygon representing the path.
|
||||||
detour = parameters['detour factor']
|
|
||||||
speed = parameters['average walking speed']
|
Args:
|
||||||
|
landmarks (List[Landmark]): List of all landmarks including 'start' and 'finish'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[Landmark], Polygon]: A tuple where the first element is the list of landmarks in the order they
|
||||||
|
should be visited, and the second element is a `Polygon` representing
|
||||||
|
the path connecting all landmarks.
|
||||||
|
"""
|
||||||
|
|
||||||
# Step 1: Find 'start' and 'finish' landmarks
|
# Step 1: Find 'start' and 'finish' landmarks
|
||||||
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
|
start_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'start')
|
||||||
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
|
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'finish')
|
||||||
|
|
||||||
start_landmark = landmarks[start_idx]
|
start_landmark = landmarks[start_idx]
|
||||||
finish_landmark = landmarks[finish_idx]
|
finish_landmark = landmarks[finish_idx]
|
||||||
@ -93,7 +177,7 @@ def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> Tuple
|
|||||||
|
|
||||||
# Step 4: Use nearest neighbor heuristic to visit all landmarks
|
# Step 4: Use nearest neighbor heuristic to visit all landmarks
|
||||||
while unvisited_landmarks:
|
while unvisited_landmarks:
|
||||||
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_time(current_landmark.location, lm.location, detour, speed))
|
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_time(current_landmark.location, lm.location))
|
||||||
path.append(nearest_landmark)
|
path.append(nearest_landmark)
|
||||||
coordinates.append(nearest_landmark.location)
|
coordinates.append(nearest_landmark.location)
|
||||||
current_landmark = nearest_landmark
|
current_landmark = nearest_landmark
|
||||||
@ -109,24 +193,51 @@ def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> Tuple
|
|||||||
|
|
||||||
|
|
||||||
# Returns a list of minor landmarks around the planned path to enhance experience
|
# Returns a list of minor landmarks around the planned path to enhance experience
|
||||||
def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[Landmark], width: float) -> List[Landmark] :
|
def get_minor_landmarks(self, all_landmarks: List[Landmark], visited_landmarks: List[Landmark], width: float) -> List[Landmark] :
|
||||||
|
"""
|
||||||
|
Identify landmarks within a specified corridor that have not been visited yet.
|
||||||
|
|
||||||
|
This function creates a corridor around the path defined by visited landmarks and then finds landmarks that fall
|
||||||
|
within this corridor. It returns a list of these landmarks, excluding those already visited, sorted by their importance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_landmarks (List[Landmark]): List of all available landmarks.
|
||||||
|
visited_landmarks (List[Landmark]): List of landmarks that have already been visited.
|
||||||
|
width (float): Width of the corridor around the visited landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Landmark]: List of important landmarks within the corridor that have not been visited yet.
|
||||||
|
"""
|
||||||
|
|
||||||
second_order_landmarks = []
|
second_order_landmarks = []
|
||||||
visited_names = []
|
visited_names = []
|
||||||
area = create_corridor(visited_landmarks, width)
|
area = self.create_corridor(visited_landmarks, width)
|
||||||
|
|
||||||
for visited in visited_landmarks :
|
for visited in visited_landmarks :
|
||||||
visited_names.append(visited.name)
|
visited_names.append(visited.name)
|
||||||
|
|
||||||
for landmark in all_landmarks :
|
for landmark in all_landmarks :
|
||||||
if is_in_area(area, landmark.location) and landmark.name not in visited_names:
|
if self.is_in_area(area, landmark.location) and landmark.name not in visited_names:
|
||||||
second_order_landmarks.append(landmark)
|
second_order_landmarks.append(landmark)
|
||||||
|
|
||||||
return take_most_important(second_order_landmarks, len(visited_landmarks))
|
return take_most_important(second_order_landmarks, len(visited_landmarks))
|
||||||
|
|
||||||
|
|
||||||
# Try fix the shortest path using shapely
|
# Try fix the shortest path using shapely
|
||||||
def fix_using_polygon(tour: List[Landmark])-> List[Landmark] :
|
def fix_using_polygon(self, tour: List[Landmark])-> List[Landmark] :
|
||||||
|
"""
|
||||||
|
Improve the tour path using geometric methods to ensure it follows a more optimal shape.
|
||||||
|
|
||||||
|
This function creates a polygon from the given tour and attempts to refine it using a concave hull. It reorders
|
||||||
|
the landmarks to fit within this refined polygon and adjusts the tour to ensure the 'start' landmark is at the
|
||||||
|
beginning. It also checks if the final polygon is simple and rearranges the tour if necessary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tour (List[Landmark]): List of landmarks representing the current tour path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Landmark]: Refined list of landmarks in the order of visit to produce a better tour path.
|
||||||
|
"""
|
||||||
|
|
||||||
coords = []
|
coords = []
|
||||||
coords_dict = {}
|
coords_dict = {}
|
||||||
@ -173,18 +284,24 @@ def fix_using_polygon(tour: List[Landmark])-> List[Landmark] :
|
|||||||
|
|
||||||
# Rearrange only if polygon still not simple
|
# Rearrange only if polygon still not simple
|
||||||
if not better_tour_poly.is_simple :
|
if not better_tour_poly.is_simple :
|
||||||
better_tour = rearrange(better_tour)
|
better_tour = self.rearrange(better_tour)
|
||||||
|
|
||||||
return better_tour
|
return better_tour
|
||||||
|
|
||||||
|
|
||||||
# Second stage of the optimization. Use linear programming again to refine the path
|
# Second stage of the optimization. Use linear programming again to refine the path
|
||||||
def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, detour: int, print_infos: bool) -> List[Landmark] :
|
def refine_optimization(self) -> List[Landmark] :
|
||||||
|
"""
|
||||||
|
Refine the initial tour path by considering additional minor landmarks and optimizing the path.
|
||||||
|
|
||||||
# Read from the file
|
This method evaluates the need for further optimization based on the initial tour. If a detour is required, it adds
|
||||||
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
|
minor landmarks around the initial predicted path and solves a new optimization problem to find a potentially better
|
||||||
parameters = json.loads(f.read())
|
tour. It then links the new tour and adjusts it using a nearest neighbor heuristic and polygon-based methods to
|
||||||
max_landmarks = parameters['max landmarks'] + 4
|
ensure a valid path. The final tour is chosen based on the shortest distance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Landmark]: The refined list of landmarks representing the optimized tour path.
|
||||||
|
"""
|
||||||
|
|
||||||
# No need to refine if no detour is taken
|
# No need to refine if no detour is taken
|
||||||
# if detour == 0 :
|
# if detour == 0 :
|
||||||
@ -192,18 +309,20 @@ def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], ma
|
|||||||
new_tour = base_tour
|
new_tour = base_tour
|
||||||
|
|
||||||
else :
|
else :
|
||||||
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
|
minor_landmarks = self.get_minor_landmarks(self.all_landmarks, self.base_tour, 200)
|
||||||
|
|
||||||
if print_infos : print("Using " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
|
self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path")
|
||||||
|
|
||||||
# full set of visitable landmarks
|
# full set of visitable landmarks
|
||||||
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
full_set = self.base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
|
||||||
full_set.append(base_tour[-1]) # add finish back
|
full_set.append(self.base_tour[-1]) # add finish back
|
||||||
|
|
||||||
# get a new tour
|
# get a new tour
|
||||||
new_tour = solve_optimization(full_set, max_time+detour, False, max_landmarks)
|
optimizer = Optimizer(self.max_time + self.detour, full_set)
|
||||||
|
new_tour = optimizer.solve_optimization()
|
||||||
|
|
||||||
if new_tour is None :
|
if new_tour is None :
|
||||||
new_tour = base_tour
|
new_tour = self.base_tour
|
||||||
|
|
||||||
# Link the new tour
|
# Link the new tour
|
||||||
new_tour, new_dist = link_list_simple(new_tour)
|
new_tour, new_dist = link_list_simple(new_tour)
|
||||||
@ -213,11 +332,11 @@ def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], ma
|
|||||||
return new_tour
|
return new_tour
|
||||||
|
|
||||||
# Find shortest path using the nearest neighbor heuristic
|
# Find shortest path using the nearest neighbor heuristic
|
||||||
better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour)
|
better_tour, better_poly = self.find_shortest_path_through_all_landmarks(new_tour)
|
||||||
|
|
||||||
# Fix the tour using Polygons if the path looks weird
|
# Fix the tour using Polygons if the path looks weird
|
||||||
if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
|
if self.base_tour[0].location == self.base_tour[-1].location and not better_poly.is_valid :
|
||||||
better_tour = fix_using_polygon(better_tour)
|
better_tour = self.fix_using_polygon(better_tour)
|
||||||
|
|
||||||
# Link the tour again
|
# Link the tour again
|
||||||
better_tour, better_dist = link_list_simple(better_tour)
|
better_tour, better_dist = link_list_simple(better_tour)
|
||||||
@ -228,16 +347,7 @@ def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], ma
|
|||||||
else :
|
else :
|
||||||
final_tour = better_tour
|
final_tour = better_tour
|
||||||
|
|
||||||
if print_infos :
|
self.logger.info("Refined tour (result of second stage optimization): ")
|
||||||
print("\n\n\nRefined tour (result of second stage optimization): ")
|
|
||||||
print_res(final_tour)
|
|
||||||
total_score = 0
|
|
||||||
for elem in final_tour :
|
|
||||||
total_score += elem.attractiveness
|
|
||||||
|
|
||||||
print("\nTotal score : " + str(total_score))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return final_tour
|
return final_tour
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ class Landmark(BaseModel) :
|
|||||||
|
|
||||||
# Properties of the landmark
|
# Properties of the landmark
|
||||||
name : str
|
name : str
|
||||||
type: Literal['sightseeing', 'nature', 'shopping']
|
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
|
||||||
location : tuple
|
location : tuple
|
||||||
osm_type : str
|
osm_type : str
|
||||||
osm_id : int
|
osm_id : int
|
||||||
@ -22,8 +22,8 @@ class Landmark(BaseModel) :
|
|||||||
uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP
|
uuid: str = Field(default_factory=uuid4) # TODO implement this ASAP
|
||||||
|
|
||||||
# Additional properties depending on specific tour
|
# Additional properties depending on specific tour
|
||||||
must_do : bool
|
must_do : Optional[bool] = False
|
||||||
must_avoid : bool
|
must_avoid : Optional[bool] = False
|
||||||
is_secondary : Optional[bool] = False # TODO future
|
is_secondary : Optional[bool] = False # TODO future
|
||||||
|
|
||||||
time_to_reach_next : Optional[int] = 0 # TODO fix this in existing code
|
time_to_reach_next : Optional[int] = 0 # TODO fix this in existing code
|
||||||
|
@ -3,7 +3,7 @@ from typing import Optional, Literal
|
|||||||
|
|
||||||
class Preference(BaseModel) :
|
class Preference(BaseModel) :
|
||||||
name: str
|
name: str
|
||||||
type: Literal['sightseeing', 'nature', 'shopping']
|
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
|
||||||
score: int # score could be from 1 to 5
|
score: int # score could be from 1 to 5
|
||||||
|
|
||||||
# Input for optimization
|
# Input for optimization
|
||||||
|
@ -4,10 +4,9 @@ from typing import List
|
|||||||
from landmarks_manager import LandmarkManager
|
from landmarks_manager import LandmarkManager
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
from optimizer_v4 import solve_optimization
|
from optimizer import Optimizer
|
||||||
from refiner import refine_optimization
|
from refiner import Refiner
|
||||||
from structs.landmarks import Landmark
|
from structs.landmarks import Landmark
|
||||||
from structs.landmarktype import LandmarkType
|
|
||||||
from structs.preferences import Preferences, Preference
|
from structs.preferences import Preferences, Preference
|
||||||
|
|
||||||
|
|
||||||
@ -24,58 +23,66 @@ def write_data(L: List[Landmark], file_name: str):
|
|||||||
data.to_json(file_name, indent = 2, force_ascii=False)
|
data.to_json(file_name, indent = 2, force_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
def test4(coordinates: tuple[float, float]) -> List[Landmark]:
|
def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = None) -> List[Landmark]:
|
||||||
|
|
||||||
|
|
||||||
manager = LandmarkManager()
|
|
||||||
|
|
||||||
preferences = Preferences(
|
preferences = Preferences(
|
||||||
sightseeing=Preference(
|
sightseeing=Preference(
|
||||||
name='sightseeing',
|
name='sightseeing',
|
||||||
type=LandmarkType(landmark_type='sightseeing'),
|
type='sightseeing',
|
||||||
score = 5),
|
score = 5),
|
||||||
nature=Preference(
|
nature=Preference(
|
||||||
name='nature',
|
name='nature',
|
||||||
type=LandmarkType(landmark_type='nature'),
|
type='nature',
|
||||||
score = 0),
|
score = 0),
|
||||||
shopping=Preference(
|
shopping=Preference(
|
||||||
name='shopping',
|
name='shopping',
|
||||||
type=LandmarkType(landmark_type='shopping'),
|
type='shopping',
|
||||||
score = 0))
|
score = 0),
|
||||||
|
|
||||||
|
max_time_minute=180,
|
||||||
|
detour_tolerance_minute=0
|
||||||
|
)
|
||||||
|
|
||||||
# Create start and finish
|
# Create start and finish
|
||||||
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=coordinates, osm_type='start', osm_id=0, attractiveness=0, must_do=False, n_tags = 0)
|
if finish_coords is None :
|
||||||
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=coordinates, osm_type='finish', osm_id=0, attractiveness=0, must_do=False, n_tags = 0)
|
finish_coords = start_coords
|
||||||
|
start = Landmark(name='start', type='start', location=start_coords, osm_type='', osm_id=0, attractiveness=0, n_tags = 0)
|
||||||
|
finish = Landmark(name='finish', type='start', location=finish_coords, osm_type='', osm_id=0, attractiveness=0, n_tags = 0)
|
||||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8777055, 2.3640967), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8777055, 2.3640967), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
#start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.847132, 2.312359), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
#start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.847132, 2.312359), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.843185, 2.344533), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.843185, 2.344533), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.847132, 2.312359), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.847132, 2.312359), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
|
||||||
|
|
||||||
|
|
||||||
|
manager = LandmarkManager(preferences=preferences, coordinates=start_coords)
|
||||||
|
|
||||||
# Generate the landmarks from the start location
|
# Generate the landmarks from the start location
|
||||||
landmarks, landmarks_short = generate_landmarks(preferences=preferences, coordinates=start.location)
|
landmarks, landmarks_short = manager.generate_landmarks_list()
|
||||||
|
|
||||||
|
# Store data to file for debug purposes
|
||||||
#write_data(landmarks, "landmarks_Wien.txt")
|
#write_data(landmarks, "landmarks_Wien.txt")
|
||||||
|
|
||||||
# Insert start and finish to the landmarks list
|
# Insert start and finish to the landmarks list
|
||||||
landmarks_short.insert(0, start)
|
landmarks_short.insert(0, start)
|
||||||
landmarks_short.append(finish)
|
landmarks_short.append(finish)
|
||||||
|
|
||||||
max_walking_time = 180 # minutes
|
|
||||||
detour = 0 # minutes
|
|
||||||
|
|
||||||
# First stage optimization
|
# First stage optimization
|
||||||
base_tour = solve_optimization(landmarks_short, max_walking_time, True)
|
optimizer = Optimizer(max_time=preferences.max_time_minute, landmarks=landmarks_short)
|
||||||
|
base_tour = optimizer.solve_optimization()
|
||||||
|
|
||||||
# Second stage using linear optimization
|
# Second stage using linear optimization
|
||||||
|
refiner = Refiner(max_time = preferences.max_time_minute, detour = preferences.detour_tolerance_minute, all_landmarks=landmarks, base_tour=base_tour)
|
||||||
refined_tour = refine_optimization(landmarks, base_tour, max_walking_time, detour, True)
|
refined_tour = refiner.refine_optimization()
|
||||||
|
|
||||||
|
|
||||||
return refined_tour
|
return refined_tour
|
||||||
|
|
||||||
|
|
||||||
#test4(tuple((48.8344400, 2.3220540))) # Café Chez César
|
#test(tuple((48.8344400, 2.3220540))) # Café Chez César
|
||||||
#test4(tuple((48.8375946, 2.2949904))) # Point random
|
#test(tuple((48.8375946, 2.2949904))) # Point random
|
||||||
#test4(tuple((47.377859, 8.540585))) # Zurich HB
|
#test(tuple((47.377859, 8.540585))) # Zurich HB
|
||||||
#test4(tuple((45.7576485, 4.8330241))) # Lyon Bellecour
|
#test(tuple((45.7576485, 4.8330241))) # Lyon Bellecour
|
||||||
test4(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
|
test(tuple((48.5848435, 7.7332974))) # Strasbourg Gare
|
||||||
#test4(tuple((48.2067858, 16.3692340))) # Vienne
|
#test(tuple((48.2067858, 16.3692340))) # Vienne
|
||||||
|
106
backend/src/utils.py
Normal file
106
backend/src/utils.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import yaml
|
||||||
|
from typing import List, Tuple
|
||||||
|
from geopy.distance import geodesic
|
||||||
|
|
||||||
|
from structs.landmarks import Landmark
|
||||||
|
import constants
|
||||||
|
|
||||||
|
def get_time(p1: Tuple[float, float], p2: Tuple[float, float]) -> int :
|
||||||
|
"""
|
||||||
|
Calculate the time in minutes to travel from one location to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
p1 (Tuple[float, float]): Coordinates of the starting location.
|
||||||
|
p2 (Tuple[float, float]): Coordinates of the destination.
|
||||||
|
detour (float): Detour factor affecting the distance.
|
||||||
|
speed (float): Walking speed in kilometers per hour.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Time to travel from p1 to p2 in minutes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||||
|
parameters = yaml.safe_load(f)
|
||||||
|
detour_factor = parameters['detour_factor']
|
||||||
|
average_walking_speed = parameters['average_walking_speed']
|
||||||
|
|
||||||
|
# Compute the straight-line distance in km
|
||||||
|
if p1 == p2 :
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
dist = geodesic(p1, p2).kilometers
|
||||||
|
|
||||||
|
# Consider the detour factor for average cityto deterline walking distance (in km)
|
||||||
|
walk_dist = dist*detour_factor
|
||||||
|
|
||||||
|
# Time to walk this distance (in minutes)
|
||||||
|
walk_time = walk_dist/average_walking_speed*60
|
||||||
|
|
||||||
|
return round(walk_time)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Same as link_list but does it on a already ordered list
|
||||||
|
def link_list_simple(ordered_visit: List[Landmark])-> List[Landmark] :
|
||||||
|
|
||||||
|
L = []
|
||||||
|
j = 0
|
||||||
|
total_dist = 0
|
||||||
|
while j < len(ordered_visit)-1 :
|
||||||
|
elem = ordered_visit[j]
|
||||||
|
next = ordered_visit[j+1]
|
||||||
|
|
||||||
|
elem.next_uuid = next.uuid
|
||||||
|
d = get_time(elem.location, next.location)
|
||||||
|
elem.time_to_reach_next = d
|
||||||
|
if elem.name not in ['start', 'finish'] :
|
||||||
|
elem.must_do = True
|
||||||
|
L.append(elem)
|
||||||
|
j += 1
|
||||||
|
total_dist += d
|
||||||
|
|
||||||
|
L.append(next)
|
||||||
|
|
||||||
|
return L, total_dist
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Take the most important landmarks from the list
|
||||||
|
def take_most_important(landmarks: List[Landmark], N_important) -> List[Landmark] :
|
||||||
|
|
||||||
|
L = len(landmarks)
|
||||||
|
L_copy = []
|
||||||
|
L_clean = []
|
||||||
|
scores = [0]*len(landmarks)
|
||||||
|
names = []
|
||||||
|
name_id = {}
|
||||||
|
|
||||||
|
for i, elem in enumerate(landmarks) :
|
||||||
|
if elem.name not in names :
|
||||||
|
names.append(elem.name)
|
||||||
|
name_id[elem.name] = [i]
|
||||||
|
L_copy.append(elem)
|
||||||
|
else :
|
||||||
|
name_id[elem.name] += [i]
|
||||||
|
scores = []
|
||||||
|
for j in name_id[elem.name] :
|
||||||
|
scores.append(L[j].attractiveness)
|
||||||
|
best_id = max(range(len(scores)), key=scores.__getitem__)
|
||||||
|
t = name_id[elem.name][best_id]
|
||||||
|
if t == i :
|
||||||
|
for old in L_copy :
|
||||||
|
if old.name == elem.name :
|
||||||
|
old.attractiveness = L[t].attractiveness
|
||||||
|
|
||||||
|
scores = [0]*len(L_copy)
|
||||||
|
for i, elem in enumerate(L_copy) :
|
||||||
|
scores[i] = elem.attractiveness
|
||||||
|
|
||||||
|
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-L):]
|
||||||
|
|
||||||
|
for i, elem in enumerate(L_copy) :
|
||||||
|
if i in res :
|
||||||
|
L_clean.append(elem)
|
||||||
|
|
||||||
|
return L_clean
|
Loading…
x
Reference in New Issue
Block a user