fixed optimizer. works fine now
All checks were successful
Build and push docker image / Build (pull_request) Successful in 2m17s
Build and release APK / Build APK (pull_request) Successful in 5m56s
Build web / Build Web (pull_request) Successful in 1m15s

This commit is contained in:
Kilian Scheidecker 2024-06-10 14:24:37 +02:00
parent c58c10b057
commit adbb6466d9
5 changed files with 20689 additions and 101 deletions

View File

@ -1,46 +1,80 @@
import math as m
from OSMPythonTools.api import Api from OSMPythonTools.api import Api
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder, Nominatim from OSMPythonTools.overpass import Overpass, overpassQueryBuilder, Nominatim
from dataclasses import dataclass
from pydantic import BaseModel
import math as m
from structs.landmarks import Landmark, LandmarkType from structs.landmarks import Landmark, LandmarkType
from structs.preferences import Preferences, Preference from structs.preferences import Preferences, Preference
from typing import List from typing import List
from typing import Tuple from typing import Tuple
RADIUS = 0.0005 # size of the bbox in degrees. 0.0005 ~ 50m
BBOX_SIDE = 10 # size of bbox in km for general area, 10km BBOX_SIDE = 10 # size of bbox in *km* for general area, 10km
RADIUS_CLOSE_TO = 50 # size of area in m for close features, 5àm radius RADIUS_CLOSE_TO = 25 # size of area in *m* for close features, 5àm radius
MIN_SCORE = 100 # discard elements with score < 100 MIN_SCORE = 30 # discard elements with score < 100
MIN_TAGS = 5 # discard elements withs less than 5 tags MIN_TAGS = 5 # discard elements withs less than 5 tags
SIGHTSEEING = LandmarkType(landmark_type='sightseeing')
NATURE = LandmarkType(landmark_type='nature')
SHOPPING = LandmarkType(landmark_type='shopping')
# Include th json here
# Include the json here
# Create a list of all things to visit given some preferences and a city. Ready for the optimizer # Create a list of all things to visit given some preferences and a city. Ready for the optimizer
def generate_landmarks(coordinates: Tuple[float, float], preferences: Preferences) : def generate_landmarks(preferences: Preferences, city_country: str = None, coordinates: Tuple[float, float] = None)->Tuple[List[Landmark], List[Landmark]] :
l_sights = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", "'amenity'='place_of_worship'", "'amenity'='fountain'", '"water"="reflecting_pool"'] l_sights = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", "'amenity'='place_of_worship'", "'amenity'='fountain'", '"water"="reflecting_pool"']
l_nature = ["'leisure'='park'", 'geological', "'natural'='geyser'", "'natural'='hot_spring'", '"natural"="arch"', '"natural"="cave_entrance"', '"natural"="volcano"', '"natural"="stone"', '"tourism"="alpine_hut"', '"tourism"="picnic_site"', '"tourism"="viewpoint"', '"tourism"="zoo"', '"waterway"="waterfall"'] l_nature = ["'leisure'='park'", 'geological', "'natural'='geyser'", "'natural'='hot_spring'", '"natural"="arch"', '"natural"="cave_entrance"', '"natural"="volcano"', '"natural"="stone"', '"tourism"="alpine_hut"', '"tourism"="picnic_site"', '"tourism"="viewpoint"', '"tourism"="zoo"', '"waterway"="waterfall"']
l_shop = ["'shop'='department_store'", "'shop'='mall'"] #, '"shop"="collector"', '"shop"="antiques"'] l_shop = ["'shop'='department_store'", "'shop'='mall'"] #, '"shop"="collector"', '"shop"="antiques"']
# List for sightseeing L = []
L1 = get_landmarks(coordinates, l_sights, LandmarkType(landmark_type='sightseeing'))
correct_score(L1, preferences.sightseeing)
# List for nature
L2 = get_landmarks(coordinates, l_nature, LandmarkType(landmark_type='nature'))
correct_score(L2, preferences.nature)
# List for shopping
L3 = get_landmarks(coordinates, l_shop, LandmarkType(landmark_type='shopping'))
correct_score(L3, preferences.shopping)
L = L1 + L2 + L3 # Use 'City, Country'
if city_country is not None :
return cleanup_list(L) # List for sightseeing
if preferences.sightseeing.score != 0 :
L1 = get_landmarks_nominatim(city_country, l_sights, SIGHTSEEING)
correct_score(L1, preferences.sightseeing)
L += L1
# List for nature
if preferences.nature.score != 0 :
L2 = get_landmarks_nominatim(city_country, l_nature, NATURE)
correct_score(L2, preferences.nature)
L += L2
# List for shopping
if preferences.shopping.score != 0 :
L3 = get_landmarks_nominatim(city_country, l_shop, SHOPPING)
correct_score(L3, preferences.shopping)
L += L3
# Use coordinates
elif coordinates is not None :
# List for sightseeing
if preferences.sightseeing.score != 0 :
L1 = get_landmarks_coords(coordinates, l_sights, SIGHTSEEING)
correct_score(L1, preferences.sightseeing)
L += L1
# List for nature
if preferences.nature.score != 0 :
L2 = get_landmarks_coords(coordinates, l_nature, NATURE)
correct_score(L2, preferences.nature)
L += L2
# List for shopping
if preferences.shopping.score != 0 :
L3 = get_landmarks_coords(coordinates, l_shop, SHOPPING)
correct_score(L3, preferences.shopping)
L += L3
return L, cleanup_list(L)
# Determines if two locations are close to each other # Determines if two locations are close to each other
def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float]) : def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float])->bool :
alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi) alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi)
if abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1]) < alpha*2 : if abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1]) < alpha*2 :
@ -49,7 +83,7 @@ def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float]) :
return False return False
# Remove duplicate elements and elements with low score # Remove duplicate elements and elements with low score
def cleanup_list(L: List[Landmark]) : def cleanup_list(L: List[Landmark])->List[Landmark] :
L_clean = [] L_clean = []
names = [] names = []
@ -70,7 +104,6 @@ def cleanup_list(L: List[Landmark]) :
return L_clean return L_clean
# Correct the score of a list of landmarks by taking into account preferences and the number of tags # Correct the score of a list of landmarks by taking into account preferences and the number of tags
def correct_score(L: List[Landmark], preference: Preference) : def correct_score(L: List[Landmark], preference: Preference) :
@ -81,8 +114,8 @@ def correct_score(L: List[Landmark], preference: Preference) :
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 {L[0].name}")
for elem in L : for elem in L :
elem.attractiveness = int(elem.attractiveness/100) + elem.n_tags # arbitrary correction of the balance score vs number of tags elem.attractiveness = int(elem.attractiveness) + elem.n_tags*100 # arbitrary correction of the balance score vs number of tags
elem.attractiveness = elem.attractiveness*preference.score # arbitrary computation elem.attractiveness = int(elem.attractiveness*preference.score/500) # arbitrary computation
# Correct the score of a list of landmarks by taking into account preferences and the number of tags # Correct the score of a list of landmarks by taking into account preferences and the number of tags
def correct_score_test(L: List[Landmark], preference: Preference) : def correct_score_test(L: List[Landmark], preference: Preference) :
@ -103,7 +136,9 @@ def count_elements_within_radius(coordinates: Tuple[float, float]) -> int:
lat = coordinates[0] lat = coordinates[0]
lon = coordinates[1] lon = coordinates[1]
bbox = {'latLower':lat-RADIUS,'lonLower':lon-RADIUS,'latHigher':lat+RADIUS,'lonHigher': lon+RADIUS} alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi)
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
overpass = Overpass() overpass = Overpass()
# Build the query to find elements within the radius # Build the query to find elements within the radius
@ -148,11 +183,11 @@ 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
# Generates the list of landmarks for a given Landmarktype. Needs coordinates, a list of amenities and the corresponding LandmarkType # Generates the list of landmarks for a given Landmarktype. Needs coordinates, a list of amenities and the corresponding LandmarkType
def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarktype: LandmarkType): def get_landmarks_coords(coordinates: Tuple[float, float], l: List[Landmark], landmarktype: LandmarkType)->List[Landmark]:
overpass = Overpass() overpass = Overpass()
# Generate a bbox around currunt coordinates # Generate a bbox around current coordinates
bbox = create_bbox(coordinates, BBOX_SIDE) bbox = create_bbox(coordinates, BBOX_SIDE)
# Initialize some variables # Initialize some variables
@ -164,6 +199,46 @@ def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarkt
result = overpass.query(query) result = overpass.query(query)
N += result.countElements() N += result.countElements()
for elem in result.elements():
name = elem.tag('name') # Add name, decode to ASCII
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
# skip if unprecise location
if name is None or location[0] is None:
continue
else :
osm_type = elem.type() # Add type : 'way' or 'relation'
osm_id = elem.id() # Add OSM id
elem_type = landmarktype # Add the landmark type as 'sightseeing
n_tags = len(elem.tags().keys()) # Add number of tags
# Add score of given landmark based on the number of surrounding elements
score = count_elements_within_radius(location)
if score is not None :
# Generate the landmark and append it to the list
landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=n_tags)
L.append(landmark)
return L
def get_landmarks_nominatim(city_country: str, l: List[Landmark], landmarktype: LandmarkType)->List[Landmark] :
overpass = Overpass()
nominatim = Nominatim()
areaId = nominatim.query(city_country).areaId()
# Initialize some variables
N = 0
L = []
for amenity in l :
query = overpassQueryBuilder(area=areaId, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
result = overpass.query(query)
N += result.countElements()
for elem in result.elements(): for elem in result.elements():
name = elem.tag('name') # Add name name = elem.tag('name') # Add name
@ -188,4 +263,3 @@ def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarkt
L.append(landmark) L.append(landmark)
return L return L

View File

@ -12,13 +12,13 @@ app = FastAPI()
# Assuming frontend is calling like this : # Assuming frontend is calling like this :
#"http://127.0.0.1:8000/process?param1={param1}&param2={param2}" #"http://127.0.0.1:8000/process?param1={param1}&param2={param2}"
# This should become main at some point @app.post("/optimizer_coords/{longitude}/{latitude}/{city_country}")
@app.post("/optimizer/{longitude}/{latitude}") def main1(preferences: Preferences = Body(...), longitude: float = None, latitude: float = None, city_country: str = None) -> List[Landmark]:
def main(longitude: float, latitude: float, preferences: Preferences = Body(...)) -> List[Landmark]:
# From frontend get longitude, latitude and prefence list # From frontend get longitude, latitude and prefence list
# Generate the landmark list # Generate the landmark list
landmarks = generate_landmarks(tuple((longitude, latitude)), preferences) landmarks = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=tuple((longitude, latitude)))
# Set the max distance # Set the max distance
max_steps = 90 max_steps = 90

View File

@ -1,8 +1,40 @@
from scipy.optimize import linprog
import numpy as np import numpy as np
from typing import List
from typing import Tuple
from scipy.optimize import linprog
from scipy.linalg import block_diag from scipy.linalg import block_diag
from structs.landmarks import Landmark, LandmarkType from structs.landmarks import Landmark
from structs.preferences import Preference, Preferences from math import radians, sin, cos, acos
DETOUR_FACTOR = 1.3 # detour factor for straightline distance
AVG_WALKING_SPEED = 4.8 # average walking speed in km/h
# Function that returns the distance in meters from one location to another
def get_distance(p1: Tuple[float, float], p2: Tuple[float, float]) :
# Compute the straight-line distance in km
if p1 == p2 :
return 0, 0
else:
dist = 6371.01 * acos(sin(radians(p1[0]))*sin(radians(p2[0])) + cos(radians(p1[0]))*cos(radians(p2[0]))*cos(radians(p1[1]) - radians(p2[1])))
# Consider the detour factor for average city
wdist = dist*DETOUR_FACTOR
# Time to walk this distance (in minutes)
wtime = wdist/AVG_WALKING_SPEED*60
if wtime > 15 :
wtime = 5*round(wtime/5)
else :
wtime = round(wtime)
return round(wdist, 1), wtime
# landmarks = [Landmark_1, Landmark_2, ...] # landmarks = [Landmark_1, Landmark_2, ...]
@ -35,10 +67,10 @@ def untangle(resx: list) -> list:
return order return order
# Just to print the result # Just to print the result
def print_res(res, landmarks: list, P) -> list: def print_res(res, order, landmarks: List[Landmark], P) -> list:
X = abs(res.x)
order = untangle(X)
things = [] things = []
"""N = int(np.sqrt(len(X))) """N = int(np.sqrt(len(X)))
@ -49,7 +81,7 @@ def print_res(res, landmarks: list, P) -> list:
for i,x in enumerate(X) : X[i] = round(x,0) for i,x in enumerate(X) : X[i] = round(x,0)
print(order)""" print(order)"""
if (X.sum()+1)**2 == len(X) : if len(order) == len(landmarks):
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ') print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
else : else :
print('Could not visit all the landmarks, the following order is suggested : ') print('Could not visit all the landmarks, the following order is suggested : ')
@ -59,10 +91,141 @@ def print_res(res, landmarks: list, P) -> list:
things.append(landmarks[idx].name) things.append(landmarks[idx].name)
steps = path_length(P, abs(res.x)) steps = path_length(P, abs(res.x))
print("\nSteps walked : " + str(steps)) print("\nMinutes walked : " + str(steps))
print(f"\nVisited {len(order)} out of {len(landmarks)} landmarks")
return things return things
# prevent the creation of similar circles
def prevent_circle(resx, landmarks: List[Landmark], A_ub, b_ub) -> bool:
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()
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
A_ub = np.vstack((A_ub, h))
b_ub.append(len(vertices_visited)-1)
return A_ub, b_ub
def break_circle2(circle_vertices, landmarks: List[Landmark], A_ub, b_ub) -> bool:
L = len(landmarks) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
if L-1 in circle_vertices :
circle_vertices.remove(L-1)
ones = [1]*L
h = [0]*L*L
for i in range(L) :
if i in circle_vertices :
h[i*L:i*L+L] = ones
A_ub = np.vstack((A_ub, h))
b_ub.append(len(circle_vertices)-1)
return A_ub, b_ub
# Checks if the path is connected
def is_connected(resx, landmarks: List[Landmark]) -> bool:
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()
edges = []
edges_visited = []
vertices_visited = []
edge1 = (ind_a[0], ind_b[0])
edges_visited.append(edge1)
vertices_visited.append(edge1[0])
for i, a in enumerate(ind_a) :
edges.append((a, ind_b[i])) # Create the list of edges
flag = False
remaining = edges
remaining.remove(edge1)
# This can be further optimized
#while len(vertices_visited) < n_edges + 1 :
break_flag = False
while len(remaining) > 0 and not break_flag:
for edge2 in remaining :
if edge2[0] == edge1[1] :
if edge1[1] in vertices_visited :
edges_visited.append(edge2)
break_flag = True
break
#continue # continue vs break vs needed at all ?
else :
vertices_visited.append(edge1[1])
edges_visited.append(edge2)
remaining.remove(edge2)
edge1 = edge2
elif edge1[1] == L-1 or edge1[1] in vertices_visited:
break_flag = True
break
#break
#if flag is True :
# break
vertices_visited.append(edge1[1])
if len(vertices_visited) == n_edges +1 :
flag = True
circle = []
else:
flag = False
circle = edges_visited
"""j = 0
for i in vertices_visited :
if landmarks[i].name == 'start' :
ordered_visit = vertices_visited[j:] + vertices_visited[:j]
break
j+=1"""
return flag, vertices_visited, circle
# Checks for cases of circular symmetry in the result # Checks for cases of circular symmetry in the result
def has_circle(resx: list) : def has_circle(resx: list) :
N = len(resx) # length of res N = len(resx) # length of res
@ -127,13 +290,13 @@ def break_sym(N, A_ub, b_ub):
return A_ub, b_ub return A_ub, b_ub
# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops # Constraint to not have circular paths. Want to go from start -> finish without unconnected loops
def break_circle(N, A_ub, b_ub, circle) : def break_circle(L, A_ub, b_ub, circle) :
l = [0]*N*N l = [0]*L*L
for index in circle : for index in circle :
x = index[0] x = index[0]
y = index[1] y = index[1]
l[x*N+y] = 1 l[x*L+y] = 1
A_ub = np.vstack((A_ub,l)) A_ub = np.vstack((A_ub,l))
b_ub.append(len(circle)-1) b_ub.append(len(circle)-1)
@ -147,19 +310,29 @@ def break_circle(N, A_ub, b_ub, circle) :
# Constraint to respect max number of travels # Constraint to respect max number of travels
def respect_number(N, A_ub, b_ub): def respect_number(N, A_ub, b_ub):
h = [] """h = []
for i in range(N) : h.append([1]*N) for i in range(N) : h.append([1]*N)
T = block_diag(*h) T = block_diag(*h)
"""for l in T : for l in T :
for i in range(7): for i in range(7):
print(l[i*7:i*7+7]) print(l[i*7:i*7+7])
print("\n")""" print("\n")"""
return np.vstack((A_ub, T)), b_ub + [1]*N #return np.vstack((A_ub, T)), b_ub + [1]*N
ones = [1]*N
zeros = [0]*N
for i in range(N) :
h = zeros*i + ones + zeros*(N-1-i)
A_ub = np.vstack((A_ub, h))
b_ub.append(1)
return A_ub, b_ub
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles # Constraint to tie the problem together. Necessary but not sufficient to avoid circles
def respect_order(N: int, A_eq, b_eq): def respect_order(N: int, A_eq, b_eq):
for i in range(N-1) : # Prevent stacked ones for i in range(N-1) : # Prevent stacked ones
if i == 0 : if i == 0 or i == N-1: # Don't touch start or finish
continue continue
else : else :
l = [0]*N l = [0]*N
@ -171,10 +344,6 @@ def respect_order(N: int, A_eq, b_eq):
A_eq = np.vstack((A_eq,l)) A_eq = np.vstack((A_eq,l))
b_eq.append(0) b_eq.append(0)
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_eq, b_eq return A_eq, b_eq
# Compute manhattan distance between 2 locations # Compute manhattan distance between 2 locations
@ -183,27 +352,41 @@ def manhattan_distance(loc1: tuple, loc2: tuple):
x2, y2 = loc2 x2, y2 = loc2
return abs(x1 - x2) + abs(y1 - y2) return abs(x1 - x2) + abs(y1 - y2)
# Constraint to not stay in position # Constraint to not stay in position. Removes d11, d22, d33, etc.
def init_eq_not_stay(N: int): def init_eq_not_stay(N: int):
l = [0]*N*N l = [0]*N*N
for i in range(N) : for i in range(N) :
for j in range(N) : for j in range(N) :
if j == i : if j == i :
l[j + i*N] = 1 l[j + i*N] = 1
l[N-1] = 1 # cannot skip from start to finish
#A_eq = np.array([np.array(xi) for xi in A_eq]) # Must convert A_eq into an np array
l = np.array(np.array(l)) l = np.array(np.array(l))
"""for i in range(7):
print(l[i*7:i*7+7])"""
return [l], [0] return [l], [0]
# Constraint to ensure start at start and finish at goal
def respect_start_finish(N, A_eq: list, b_eq: list):
ls = [1]*N + [0]*N*(N-1) # sets only horizontal ones for start (go from)
ljump = [0]*N*N
ljump[N-1] = 1 # Prevent start finish jump
lg = [0]*N*N
for k in range(N-1) : # sets only vertical ones for goal (go to)
if k != 0 : # Prevent the shortcut start -> finish
lg[k*N+N-1] = 1
A_eq = np.vstack((A_eq,ls))
A_eq = np.vstack((A_eq,ljump))
A_eq = np.vstack((A_eq,lg))
b_eq.append(1)
b_eq.append(0)
b_eq.append(1)
return A_eq, b_eq
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness # 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 # We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
def init_ub_dist(landmarks: list, max_steps: int): def init_ub_dist(landmarks: List[Landmark], max_steps: int):
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ... # Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
c = [] c = []
# Coefficients of inequality constraints (left-hand side) # Coefficients of inequality constraints (left-hand side)
@ -212,7 +395,8 @@ def init_ub_dist(landmarks: list, max_steps: int):
dist_table = [0]*len(landmarks) dist_table = [0]*len(landmarks)
c.append(-spot1.attractiveness) c.append(-spot1.attractiveness)
for j, spot2 in enumerate(landmarks) : for j, spot2 in enumerate(landmarks) :
dist_table[j] = manhattan_distance(spot1.location, spot2.location) d, t = get_distance(spot1.location, spot2.location)
dist_table[j] = t
A.append(dist_table) A.append(dist_table)
c = c*len(landmarks) c = c*len(landmarks)
A_ub = [] A_ub = []
@ -222,31 +406,31 @@ def init_ub_dist(landmarks: list, max_steps: int):
return c, A_ub, [max_steps] return c, A_ub, [max_steps]
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1 # Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) : def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) :
L = len(landmarks) L = len(landmarks)
H = 0 # sort of heuristic to get an idea of the number of steps needed H = 0 # sort of heuristic to get an idea of the number of steps needed
for i in landmarks :
if i.name == "départ" : elem_prev = i # list of all matches elem_prev = landmarks[0]
for i, elem in enumerate(landmarks) : for i, elem in enumerate(landmarks) :
if elem.attractiveness == -1 : if elem.must_do is True and elem.name not in ['finish', 'start']:
l = [0]*L*L l = [0]*L*L
if elem.name != "arrivée" : for j in range(L) : # sets the horizontal ones (go from)
for j in range(L) : l[j +i*L] = 1 # sets the vertical ones (go to) double check if good
l[j +i*L] = 1
for k in range(L-1) :
else : # This ensures we go to goal l[k*L+L-1] = 1
for k in range(L-1) :
l[k*L+L-1] = 1
H += manhattan_distance(elem.location, elem_prev.location)
elem_prev = elem
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
A_eq = np.vstack((A_eq,l)) A_eq = np.vstack((A_eq,l))
b_eq.append(1) b_eq.append(2)
d, t = get_distance(elem.location, elem_prev.location)
H += t
elem_prev = elem
return A_eq, b_eq, H return A_eq, b_eq, H
# Computes the path length given path matrix (dist_table) and a result # Computes the path length given path matrix (dist_table) and a result
@ -259,18 +443,18 @@ def solve_optimization (landmarks, max_steps, printing_details) :
N = len(landmarks) N = len(landmarks)
# SET CONSTRAINTS FOR INEQUALITY # SET CONSTRAINTS FOR INEQUALITY
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
P = A_ub # store the paths for later. Needed to compute path length P = A_ub # store the paths for later. Needed to compute path length
A_ub, b_ub = respect_number(N, A_ub, b_ub) # Respect max number of visits. A_ub, b_ub = respect_number(N, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks).
# TODO : Problems with circular symmetry # TODO : Problems with circular symmetry
A_ub, b_ub = break_sym(N, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values A_ub, b_ub = break_sym(N, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values
# SET CONSTRAINTS FOR EQUALITY # SET CONSTRAINTS FOR EQUALITY
A_eq, b_eq = init_eq_not_stay(N) # Force solution not to stay in same place A_eq, b_eq = init_eq_not_stay(N) # Force solution not to stay in same place
A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
A_eq, b_eq = respect_start_finish(N, A_eq, b_eq) # Force start and finish positions
A_eq, b_eq = respect_order(N, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor) A_eq, b_eq = respect_order(N, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
# Bounds for variables (x can only be 0 or 1) # Bounds for variables (x can only be 0 or 1)
x_bounds = [(0, 1)] * len(c) x_bounds = [(0, 1)] * len(c)
@ -298,20 +482,27 @@ def solve_optimization (landmarks, max_steps, printing_details) :
# If there is a solution, we're good to go, just check for # If there is a solution, we're good to go, just check for
else : else :
circle = has_circle(res.x) t, order, circle = is_connected(res.x, landmarks)
i = 0 i = 0
# Break the circular symmetry if needed # Break the circular symmetry if needed
while len(circle) != 0 : while len(circle) != 0 :
A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle) A_ub, b_ub = prevent_circle(res.x, landmarks, A_ub, b_ub)
A_ub, b_ub = break_circle(len(landmarks), A_ub, b_ub, circle)
A_ub, b_ub = break_circle2(order, landmarks, A_ub, b_ub)
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) 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)
circle = has_circle(res.x) t, order, circle = is_connected(res.x, landmarks)
if t :
break
#circle = has_circle(res.x)
print(i)
i += 1 i += 1
if printing_details is True : if printing_details is True :
if i != 0 : if i != 0 :
print(f"Neded to recompute paths {i} times because of unconnected loops...") print(f"Neded to recompute paths {i} times because of unconnected loops...")
X = print_res(res, landmarks, P) t, order, [] = is_connected(res.x, landmarks)
X = print_res(res, order, landmarks, P)
return X return X
else : else :
return untangle(res.x) return untangle(res.x)

View File

@ -1,13 +1,25 @@
import pandas as pd
from optimizer import solve_optimization from optimizer import solve_optimization
from landmarks_manager import generate_landmarks from landmarks_manager import generate_landmarks
from structs.landmarks import LandmarkTest
from structs.landmarks import Landmark from structs.landmarks import Landmark
from structs.landmarktype import LandmarkType from structs.landmarktype import LandmarkType
from structs.preferences import Preferences, Preference from structs.preferences import Preferences, Preference
from fastapi import FastAPI, Query, Body from fastapi.encoders import jsonable_encoder
from typing import List from typing import List
# Helper function to create a .txt file with results
def write_data(L: List[Landmark]):
data = pd.DataFrame()
i = 0
for landmark in L :
data[i] = jsonable_encoder(landmark)
i += 1
data.to_json('landmarks.txt', indent = 2, force_ascii=False)
def test3(city_country: str) -> List[Landmark]: def test3(city_country: str) -> List[Landmark]:
@ -25,7 +37,9 @@ def test3(city_country: str) -> List[Landmark]:
type=LandmarkType(landmark_type='shopping'), type=LandmarkType(landmark_type='shopping'),
score = 5)) score = 5))
landmarks = generate_landmarks(city_country, preferences) coords = None
landmarks = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=coords)
max_steps = 9 max_steps = 9
@ -47,21 +61,33 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]:
nature=Preference( nature=Preference(
name='nature', name='nature',
type=LandmarkType(landmark_type='nature'), type=LandmarkType(landmark_type='nature'),
score = 0), score = 5),
shopping=Preference( shopping=Preference(
name='shopping', name='shopping',
type=LandmarkType(landmark_type='shopping'), type=LandmarkType(landmark_type='shopping'),
score = 5)) score = 5))
landmarks = generate_landmarks(coordinates, preferences) city_country = None
max_steps = 90 landmarks, landmarks_short = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=coordinates)
visiting_order = solve_optimization(landmarks, max_steps, True) #write_data(landmarks)
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.8375946, 2.2949904), 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.8375946, 2.2949904), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
test = landmarks_short
test.append(finish)
test.insert(0, start)
max_walking_time = 4 # hours
visiting_order = solve_optimization(test, max_walking_time*60, True)
print(len(visiting_order))
return len(visiting_order) return len(visiting_order)
test3(tuple((48.834378, 2.322113))) test4(tuple((48.834378, 2.322113)))

20297
landmarks.txt Normal file

File diff suppressed because it is too large Load Diff