diff --git a/backend/src/landmarks_manager.py b/backend/src/landmarks_manager.py index 37eb032..618e169 100644 --- a/backend/src/landmarks_manager.py +++ b/backend/src/landmarks_manager.py @@ -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 diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py index 76fd0ed..d45d51b 100644 --- a/backend/src/optimizer.py +++ b/backend/src/optimizer.py @@ -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...") diff --git a/backend/src/parameters/landmarks_manager.params b/backend/src/parameters/landmarks_manager.params index 087525e..942c214 100644 --- a/backend/src/parameters/landmarks_manager.params +++ b/backend/src/parameters/landmarks_manager.params @@ -4,5 +4,5 @@ "church coeff" : 0.6, "park coeff" : 1.5, "tag coeff" : 100, - "N important" : 30 + "N important" : 40 } \ No newline at end of file diff --git a/backend/src/parameters/optimizer.params b/backend/src/parameters/optimizer.params index 5d32077..18ca240 100644 --- a/backend/src/parameters/optimizer.params +++ b/backend/src/parameters/optimizer.params @@ -1,4 +1,5 @@ { "detour factor" : 1.4, - "average walking speed" : 4.8 + "average walking speed" : 4.8, + "max landmarks" : 10 } \ No newline at end of file diff --git a/backend/src/refiner.py b/backend/src/refiner.py index b57610c..dec6101 100644 --- a/backend/src/refiner.py +++ b/backend/src/refiner.py @@ -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) - - 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 - - # 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 - - # 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 - - better_route = [] # 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) 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 - + # get a new tour + new_tour = solve_optimization(full_set, max_time, False) + new_tour, new_dist = add_time_to_reach_simple(new_tour) - # Scroll the list to have start in front again - start_index = name_index['start'] - better_route = better_route[start_index:] + better_route[:start_index] + """#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 - # 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) + # 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_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 + + # reverse the xs and ys + xs.reverse() + ys.reverse() + + 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_tour = better_tour[start_index:] + better_tour[:start_index] + + # 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 diff --git a/backend/src/tester.py b/backend/src/tester.py index 4920847..758e003 100644 --- a/backend/src/tester.py +++ b/backend/src/tester.py @@ -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') \ No newline at end of file