updated refiner
Some checks failed
Build and push docker image / Build (pull_request) Successful in 3m17s
Build and release APK / Build APK (pull_request) Has been cancelled
Build web / Build Web (pull_request) Has been cancelled

This commit is contained in:
Helldragon67 2024-06-26 10:52:24 +02:00
parent 8d068c80a7
commit fdcaaf8c16
6 changed files with 265 additions and 68 deletions

View File

@ -1,8 +1,8 @@
import math as m
import json, os
from typing import List, Tuple
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder, Nominatim
from typing import List, Tuple, Optional
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
from structs.landmarks import Landmark, LandmarkType
from structs.preferences import Preferences, Preference
@ -38,7 +38,9 @@ def generate_landmarks(preferences: Preferences, coordinates: Tuple[float, float
correct_score(L3, preferences.shopping)
L += L3
return remove_duplicates(L), take_most_important(L)
L = remove_duplicates(L)
return L, take_most_important(L)
"""def generate_landmarks(preferences: Preferences, city_country: str = None, coordinates: Tuple[float, float] = None) -> Tuple[List[Landmark], List[Landmark]] :
@ -91,7 +93,7 @@ def get_list(path: str) -> List[str] :
# Take the most important landmarks from the list
def take_most_important(L: List[Landmark]) -> List[Landmark] :
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 :
@ -125,7 +127,7 @@ def take_most_important(L: List[Landmark]) -> List[Landmark] :
for i, elem in enumerate(L_copy) :
scores[i] = elem.attractiveness
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-N_important:]
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-N):]
for i, elem in enumerate(L_copy) :
if i in res :
@ -148,21 +150,15 @@ def remove_duplicates(L: List[Landmark]) -> List[Landmark] :
L_clean = []
names = []
coords = []
for landmark in L :
if landmark.name in names and landmark.location in coords:
if landmark.name in names:
continue
approx_coords = tuple((round(landmark.location[0], 4), round(landmark.location[0], 4)))
if approx_coords in coords :
continue
else :
names.append(landmark.name)
L_clean.append(landmark)
coords.append(approx_coords)
return L_clean

View File

@ -4,12 +4,13 @@ import json, os
from typing import List, Tuple
from scipy.optimize import linprog
from math import radians, sin, cos, acos
from shapely import Polygon
from structs.landmarks import Landmark
# Function to print the result
def print_res(L: List[Landmark], L_tot) -> list:
def print_res(L: List[Landmark], L_tot):
if len(L) == L_tot:
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
@ -25,10 +26,10 @@ def print_res(L: List[Landmark], L_tot) -> list:
print('- ' + elem.name)
print("\nMinutes walked : " + str(dist))
print(f"Visited {len(L)} out of {L_tot} landmarks")
print(f"Visited {len(L)-2} out of {L_tot-2} landmarks")
# Prevent the use of a particular set of nodes
# Prevent the use of a particular solution
def prevent_config(resx, A_ub, b_ub) -> bool:
for i, elem in enumerate(resx):
@ -56,7 +57,7 @@ def prevent_config(resx, A_ub, b_ub) -> bool:
return A_ub, b_ub
# Prevent the possibility of a given set of vertices
# Prevent the possibility of a given solution bit
def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool:
if L-1 in circle_vertices :
@ -73,7 +74,7 @@ def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool:
return A_ub, b_ub
# Checks if the path is connected, returns a circle if it finds one
# Checks if the path is connected, returns a circle if it finds one and the RESULT
def is_connected(resx) -> bool:
# first round the results to have only 0-1 values
@ -182,8 +183,8 @@ def init_ub_dist(landmarks: List[Landmark], max_steps: int):
return c, A_ub, [max_steps]
# Constraint to respect max number of travels
def respect_number(L, A_ub, b_ub):
# Constraint to respect only one travel per landmark. Also caps the total number of visited landmarks
def respect_number(L:int, A_ub, b_ub):
ones = [1]*L
zeros = [0]*L
@ -192,6 +193,14 @@ def respect_number(L, A_ub, b_ub):
A_ub = np.vstack((A_ub, h))
b_ub.append(1)
# 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_ub = np.vstack((A_ub, ones*L))
b_ub.append(max_landmarks+1)
return A_ub, b_ub
@ -290,7 +299,7 @@ def respect_order(N: int, A_eq, b_eq):
return A_eq, b_eq
# Computes the path length given path matrix (dist_table) and a result
# Computes the time to reach from each landmark to the next
def add_time_to_reach(order: List[int], landmarks: List[Landmark])->List[Landmark] :
# Read the parameters from the file
@ -315,6 +324,7 @@ def add_time_to_reach(order: List[int], landmarks: List[Landmark])->List[Landmar
return L
def add_time_to_reach_simple(ordered_visit: List[Landmark])-> List[Landmark] :
# Read the parameters from the file
@ -326,14 +336,17 @@ def add_time_to_reach_simple(ordered_visit: List[Landmark])-> List[Landmark] :
L = []
prev = ordered_visit[0]
L.append(prev)
total_dist = 0
for elem in ordered_visit[1:] :
elem.time_to_reach = get_distance(elem.location, prev.location, detour_factor, speed)[1]
elem.must_do = True
L.append(elem)
prev = elem
total_dist += get_distance(elem.location, prev.location, detour_factor, speed)[1]
return L
return L, total_dist
# Main optimization pipeline
@ -366,22 +379,23 @@ def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_deta
else :
order, circle = is_connected(res.x)
i = 0
timeout = 300
timeout = 80
while len(circle) != 0 and i < timeout:
A_ub, b_ub = prevent_config(res.x, A_ub, b_ub)
A_ub, b_ub = break_cricle(order, len(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)
order, circle = is_connected(res.x)
if len(circle) == 0 :
# Add the times to reach and stop optimizing
L = add_time_to_reach(order, landmarks)
break
#print(i)
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 = add_time_to_reach(order, landmarks)
if printing_details is True :
if i != 0 :
print(f"Neded to recompute paths {i} times because of unconnected loops...")

View File

@ -4,5 +4,5 @@
"church coeff" : 0.6,
"park coeff" : 1.5,
"tag coeff" : 100,
"N important" : 30
"N important" : 40
}

View File

@ -1,4 +1,5 @@
{
"detour factor" : 1.4,
"average walking speed" : 4.8
"average walking speed" : 4.8,
"max landmarks" : 10
}

View File

@ -1,10 +1,15 @@
from collections import defaultdict
from heapq import heappop, heappush
from itertools import permutations
import os, json
from shapely import buffer, LineString, Point, Polygon, MultiPoint, convex_hull, concave_hull, LinearRing
from typing import List
from typing import List, Tuple
from math import pi
from structs.landmarks import Landmark
from landmarks_manager import take_most_important
from optimizer import solve_optimization, add_time_to_reach_simple, print_res
from optimizer import solve_optimization, add_time_to_reach_simple, print_res, get_distance
def create_corridor(landmarks: List[Landmark], width: float) :
@ -28,10 +33,135 @@ def create_linestring(landmarks: List[Landmark])->List[Point] :
def is_in_area(area: Polygon, coordinates) -> bool :
point = Point(coordinates)
return point.within(area)
def is_close_to(location1: Tuple[float], location2: Tuple[float]):
"""Determine if two locations are close by rounding their coordinates to 3 decimals."""
absx = abs(location1[0] - location2[0])
absy = abs(location1[1] - location2[1])
return absx < 0.001 and absy < 0.001
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
def rearrange(landmarks: List[Landmark]) -> List[Landmark]:
i = 1
while i < len(landmarks):
j = i+1
while j < len(landmarks):
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 they are not adjacent, move the j-th element to be adjacent to the i-th element
if j != i + 1:
landmarks.insert(i + 1, landmarks.pop(j))
break # Move to the next i-th element after rearrangement
j += 1
i += 1
return landmarks
"""
def find_shortest_path(landmarks: List[Landmark]) -> List[Landmark]:
# Read from data
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']
# Step 1: Build the graph
graph = defaultdict(list)
for i in range(len(landmarks)):
for j in range(len(landmarks)):
if i != j:
distance = get_distance(landmarks[i].location, landmarks[j].location, detour, speed)[1]
graph[i].append((distance, j))
# Step 2: Dijkstra's algorithm to find the shortest path from start to finish
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
distances = {i: float('inf') for i in range(len(landmarks))}
previous_nodes = {i: None for i in range(len(landmarks))}
distances[start_idx] = 0
priority_queue = [(0, start_idx)]
while priority_queue:
current_distance, current_index = heappop(priority_queue)
if current_distance > distances[current_index]:
continue
for neighbor_distance, neighbor_index in graph[current_index]:
distance = current_distance + neighbor_distance
if distance < distances[neighbor_index]:
distances[neighbor_index] = distance
previous_nodes[neighbor_index] = current_index
heappush(priority_queue, (distance, neighbor_index))
# Step 3: Backtrack from finish to start to find the path
path = []
current_index = finish_idx
while current_index is not None:
path.append(landmarks[current_index])
current_index = previous_nodes[current_index]
path.reverse()
return path
"""
"""
def total_path_distance(path: List[Landmark], detour, speed) -> float:
total_distance = 0
for i in range(len(path) - 1):
total_distance += get_distance(path[i].location, path[i + 1].location, detour, speed)[1]
return total_distance
"""
def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> List[Landmark]:
# Read from data
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']
# Step 1: Find 'start' and 'finish' landmarks
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
start_landmark = landmarks[start_idx]
finish_landmark = landmarks[finish_idx]
# Step 2: Create a list of unvisited landmarks excluding 'start' and 'finish'
unvisited_landmarks = [lm for i, lm in enumerate(landmarks) if i not in [start_idx, finish_idx]]
# Step 3: Initialize the path with the 'start' landmark
path = [start_landmark]
coordinates = [landmarks[start_idx].location]
current_landmark = start_landmark
# Step 4: Use nearest neighbor heuristic to visit all landmarks
while unvisited_landmarks:
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_distance(current_landmark.location, lm.location, detour, speed)[1])
path.append(nearest_landmark)
coordinates.append(nearest_landmark.location)
current_landmark = nearest_landmark
unvisited_landmarks.remove(nearest_landmark)
# Step 5: Finally add the 'finish' landmark to the path
path.append(finish_landmark)
coordinates.append(landmarks[finish_idx].location)
path_poly = Polygon(coordinates)
return path, path_poly
def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[Landmark], width: float) -> List[Landmark] :
second_order_landmarks = []
@ -45,7 +175,7 @@ def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[L
if is_in_area(area, landmark.location) and landmark.name not in visited_names:
second_order_landmarks.append(landmark)
return take_most_important(second_order_landmarks)
return take_most_important(second_order_landmarks, len(visited_landmarks))
@ -58,55 +188,106 @@ def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[L
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
full_set.append(base_tour[-1]) # add finish back
new_route = solve_optimization(full_set, max_time, print_infos)
new_tour = solve_optimization(full_set, max_time, print_infos)
return new_route"""
return new_tour"""
def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] :
# Read 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']
if len(base_tour)-2 >= max_landmarks :
return base_tour
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
if print_infos : print("There are " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
if print_infos : print("Using " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
# full set of visitable landmarks
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
full_set.append(base_tour[-1]) # add finish back
# get a new route
new_route = solve_optimization(full_set, max_time, False)
# get a new tour
new_tour = solve_optimization(full_set, max_time, False)
new_tour, new_dist = add_time_to_reach_simple(new_tour)
coords = [] # Coordinates of the new route
coords_dict = {} # maps the location of an element to the element itself. Used to access the elements back once we get the geometry
"""#if base_tour[0].location == base_tour[-1].location :
if False :
coords = [] # Coordinates of the new tour
coords_dict = {} # maps the location of an element to the element itself. Used to access the elements back once we get the geometry
# Iterate through the new route without finish
for elem in new_route[:-1] :
coords.append(Point(elem.location))
coords_dict[elem.location] = elem # if start = goal, only finish remains
# Iterate through the new tour without finish
for elem in new_tour[:-1] :
coords.append(Point(elem.location))
coords_dict[elem.location] = elem # if start = goal, only finish remains
# Create a concave polygon using the coordinates
better_route_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of route leaving out start and finish
xs, ys = better_route_poly.exterior.xy
# Create a concave polygon using the coordinates
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
xs, ys = better_tour_poly.exterior.xy
better_route = [] # List of ordered visit
name_index = {} # Maps the name of a landmark to its index in the concave polygon
# reverse the xs and ys
xs.reverse()
ys.reverse()
# Loop through the polygon and generate the better (ordered) route
for i,x in enumerate(xs[:-1]) :
better_route.append(coords_dict[tuple((x,ys[i]))])
name_index[coords_dict[tuple((x,ys[i]))].name] = i
better_tour = [] # List of ordered visit
name_index = {} # Maps the name of a landmark to its index in the concave polygon
# Loop through the polygon and generate the better (ordered) tour
for i,x in enumerate(xs[:-1]) :
better_tour.append(coords_dict[tuple((x,ys[i]))])
name_index[coords_dict[tuple((x,ys[i]))].name] = i
# Scroll the list to have start in front again
start_index = name_index['start']
better_route = better_route[start_index:] + better_route[:start_index]
# Scroll the list to have start in front again
start_index = name_index['start']
better_tour = better_tour[start_index:] + better_tour[:start_index]
# Append the finish back and correct the time to reach
better_route.append(new_route[-1])
better_route = add_time_to_reach_simple(better_route)
# Append the finish back and correct the time to reach
better_tour.append(new_tour[-1])
# Rearrange only if polygon
better_tour = rearrange(better_tour)
# Add the time to reach
better_tour = add_time_to_reach_simple(better_tour)
"""
"""
if not better_poly.is_simple :
coords_dict = {}
better_tour2 = []
for elem in better_tour :
coords_dict[elem.location] = elem
better_poly2 = better_poly.buffer(0)
new_coords = better_poly2.exterior.coords[:]
start_coords = base_tour[0].location
start_index = new_coords.
#for point in new_coords :
"""
better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour)
better_tour, better_dist = add_time_to_reach_simple(better_tour)
if new_dist < better_dist :
final_tour = new_tour
else :
final_tour = better_tour
if print_infos :
print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
print("\nRefined tour (result of second stage optimization): ")
print_res(better_route, len(better_route))
print_res(final_tour, len(full_set))
return better_route
return final_tour

View File

@ -84,6 +84,10 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]:
# 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=True, n_tags = 0)
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=coordinates, 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)
#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)
# Generate the landmarks from the start location
landmarks, landmarks_short = generate_landmarks(preferences=preferences, coordinates=start.location)
@ -94,7 +98,7 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]:
landmarks_short.append(finish)
# TODO use these parameters in another way
max_walking_time = 4 # hours
max_walking_time = 2 # hours
detour = 30 # minutes
# First stage optimization
@ -107,5 +111,6 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]:
#test4(tuple((48.8344400, 2.3220540))) # Café Chez César
test4(tuple((48.8375946, 2.2949904))) # Point random
#test4(tuple((48.8375946, 2.2949904))) # Point random
test4(tuple((47.377859, 8.540585))) # Zurich HB
#test3('Vienna, Austria')