diff --git a/backend/src/optimizer_v2.py b/backend/src/optimizer_v2.py deleted file mode 100644 index 1b89e29..0000000 --- a/backend/src/optimizer_v2.py +++ /dev/null @@ -1,290 +0,0 @@ -import networkx as nx -from typing import List, Tuple -from geopy.distance import geodesic -from scipy.spatial import KDTree -import numpy as np -from itertools import combinations -from structs.landmarks import Landmark -from optimizer_v4 import print_res, link_list_simple, get_time -import os -import json -import heapq - -# Heuristic function: distance to the goal -def heuristic(loc1: Tuple[float, float], loc2: Tuple[float, float], score2: int) -> float: - return geodesic(loc1, loc2).meters - - - -# A* planner to search through the graph -def a_star2(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed): - open_set = [] - heapq.heappush(open_set, (0, start_id, 0, [start_id], set([start_id]))) - best_path = None - max_attractiveness = 0 - visited_must_do = set() - - while open_set: - _, current_node, current_length, path, visited = heapq.heappop(open_set) - - # If current node is a must_do node and hasn't been visited yet, mark it as visited - if current_node in must_do_nodes and current_node not in visited_must_do: - visited_must_do.add(current_node) - - # Check if path includes all must_do nodes and reaches the end - if current_node == end_id and all(node in visited for node in must_do_nodes): - attractiveness = sum(G.nodes[node]['weight'] for node in path) - if attractiveness > max_attractiveness: - best_path = path - max_attractiveness = attractiveness - continue - - if len(path) > max_landmarks + 1: - continue - - for neighbor in G.neighbors(current_node): - if neighbor not in visited: - #distance = int(geodesic(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos']).meters * detour / (speed * 16.6666)) - distance = get_time(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos'], detour, speed) - if current_length + distance <= max_walking_time: - new_path = path + [neighbor] - new_visited = visited | {neighbor} - estimated_cost = current_length + distance + get_time(G.nodes[neighbor]['pos'], G.nodes[end_id]['pos'], detour, speed) - heapq.heappush(open_set, (estimated_cost, neighbor, current_length + distance, new_path, new_visited)) - - # Check if all must_do_nodes have been visited - if all(node in visited_must_do for node in must_do_nodes): - return best_path, max_attractiveness - else: - return None, 0 - - -def a_star(G, start_id, end_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed): - open_set = [] - heapq.heappush(open_set, (0, start_id, 0, [start_id], set([start_id]))) - best_path = None - max_attractiveness = 0 - visited_must_do = set() - - while open_set: - _, current_node, current_length, path, visited = heapq.heappop(open_set) - - # If current node is a must_do node and hasn't been visited yet, mark it as visited - if current_node in must_do_nodes and current_node not in visited_must_do: - visited_must_do.add(current_node) - - # Check if path includes all must_do nodes and reaches the end - if current_node == end_id and all(node in visited for node in must_do_nodes): - attractiveness = sum(G.nodes[node]['weight'] for node in path) - if attractiveness > max_attractiveness: - best_path = path - max_attractiveness = attractiveness - continue - - if len(path) > max_landmarks + 1: - continue - - for neighbor in G.neighbors(current_node): - if neighbor not in visited: - distance = get_time(G.nodes[current_node]['pos'], G.nodes[neighbor]['pos'], detour, speed) - if current_length + distance <= max_walking_time: - new_path = path + [neighbor] - new_visited = visited | {neighbor} - estimated_cost = current_length + distance + heuristic(G.nodes[neighbor]['pos'], G.nodes[end_id]['pos'], G.nodes[neighbor]['weight']) - heapq.heappush(open_set, (estimated_cost, neighbor, current_length + distance, new_path, new_visited)) - - # Check if all must_do_nodes have been visited - if all(node in visited_must_do for node in must_do_nodes): - return best_path, max_attractiveness - else: - return None, 0 - - - -def find_path(G, start_id, finish_id, max_walking_time, must_do_nodes, max_landmarks) -> List[str]: - - # 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 = parameters['detour factor'] - speed = parameters['average walking speed'] - - best_path, _ = a_star(G, start_id, finish_id, max_walking_time, must_do_nodes, max_landmarks, detour, speed) - - return best_path if best_path else [] - - - -# Function to dynamically adjust theta -def adjust_theta(num_nodes, theta_opt, target_ratio=2.0): - # Start with an initial guess - initial_theta = theta_opt - # Adjust theta to aim for the target ratio of edges to nodes - return initial_theta / (num_nodes ** (1 / target_ratio)) - - -# Create a graph using NetworkX and generate the path without must_do -def generate_path(landmarks: List[Landmark], max_walking_time: float, max_landmarks: int) -> List[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 = parameters['detour factor'] - speed = parameters['average walking speed'] - - landmap = {} - pos_dict = {} - weight_dict = {} - G = nx.Graph() - - # Add nodes to the graph with attractiveness - for i, landmark in enumerate(landmarks): - pos_dict[i] = landmark.location - weight_dict[i] = landmark.attractiveness - landmap[i] = landmark - G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) - if landmark.name == 'start' : - start_id = i - elif landmark.name == 'finish' : - finish_id = i - - coords = np.array(list(pos_dict.values())) - kdtree = KDTree(coords) - - k = 5 - if len(landmarks) <= k : - k = len(landmarks)-1 - - for node, coord in pos_dict.items(): - indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself - for idx in indices[1:]: # skip the first one (itself) - neighbor = list(pos_dict.keys())[idx] - distance = get_time(coord, pos_dict[neighbor], detour, speed) - G.add_edge(node, neighbor, weight=distance) - - print(f"Graph with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges") - print("Start computing path...") - - # Find the valid path using the greedy algorithm - valid_path = find_path(G, start_id, finish_id, max_walking_time, [], max_landmarks) - - if not valid_path: - return [] # No valid path found - - lis = [landmap[id] for id in valid_path] - - lis, _ = link_list_simple(lis) - - print_res(lis, len(landmarks)) - - - return lis - - - -# Create a graph using NetworkX and generate the path -def generate_path2(landmarks: List[Landmark], max_walking_time: float, max_landmarks: int) -> List[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 = parameters['detour factor'] - speed = parameters['average walking speed'] - - - landmap = {} - pos_dict = {} - weight_dict = {} - must_do_nodes = [] - G = nx.Graph() - # Add nodes to the graph with attractiveness - for i, landmark in enumerate(landmarks): - pos_dict[i] = landmark.location - weight_dict[i] = landmark.attractiveness - landmap[i] = landmark - if landmark.must_do : - must_do_nodes.append(i) - G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) - if landmark.name == 'start' : - start_id = i - elif landmark.name == 'finish' : - finish_id = i - - coords = np.array(list(pos_dict.values())) - kdtree = KDTree(coords) - - k = 3 - for node, coord in pos_dict.items(): - indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself - for idx in indices[1:]: # skip the first one (itself) - neighbor = list(pos_dict.keys())[idx] - distance = get_time(coord, pos_dict[neighbor], detour, speed) - G.add_edge(node, neighbor, weight=distance) - - - - # Add special edges between must_do nodes - if len(must_do_nodes) > 0 : - for node1, node2 in combinations(must_do_nodes, 2): - if not G.has_edge(node1, node2): - distance = get_time(G.nodes[node1]['pos'], G.nodes[node2]['pos'], detour, speed) - G.add_edge(node1, node2, weight=distance) - - print(f"Graph with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges") - print("Computing path...") - - # Find the valid path using the greedy algorithm - valid_path = find_path(G, start_id, finish_id, max_walking_time, must_do_nodes, max_landmarks) - - if not valid_path: - return [] # No valid path found - - lis = [landmap[id] for id in valid_path] - - lis, tot_dist = link_list_simple(lis) - - print_res(lis, len(landmarks)) - - - return lis - - - - -def correct_path(tour: 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 = parameters['detour factor'] - speed = parameters['average walking speed'] - - G = nx.Graph() - - coords = [] - landmap = {} - for i, landmark in enumerate(tour) : - coords.append(landmark.location) - landmap[i] = landmark - G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) - - kdtree = KDTree(coords) - - k = 3 - for node, coord in coords: - indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself - for idx in indices[1:]: # skip the first one (itself) - neighbor = list(coords)[idx] - distance = get_time(coord, coords[neighbor], detour, speed) - G.add_edge(node, neighbor, weight=distance) - - path = nx.approximation.traveling_salesman_problem(G, weight='weight', cycle=True) - - lis = [landmap[id] for id in path] - - lis, tot_dist = link_list_simple(lis) - - print_res(lis, len(tour)) - - return path - diff --git a/backend/src/optimizer_v3.py b/backend/src/optimizer_v3.py deleted file mode 100644 index a4545d3..0000000 --- a/backend/src/optimizer_v3.py +++ /dev/null @@ -1,326 +0,0 @@ -import numpy as np -import json, os - -from typing import List, Tuple -from itertools import combinations -from scipy.optimize import linprog -from math import radians, sin, cos, acos -from shapely import Polygon -from geopy.distance import geodesic - - -from structs.landmarks import Landmark - - -# Function to print the result -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 : ') - else : - print('Could not visit all the landmarks, 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} out of {L_tot-2} landmarks") - - -# Function that returns the distance in meters 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 m - if p1 == p2 : - return 0 - else: - #dist = 1000 * 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]))) - dist = geodesic(p1, p2).meters - - # Consider the detour factor for average city to determine walking distance (in m) - walk_dist = dist*detour - - # Time to walk this distance (in minutes) - walk_time = walk_dist/speed*(60/1000) - - """if walk_time > 15 : - walk_time = 5*round(walk_time/5) - else : - walk_time = round(walk_time)""" - - - return round(walk_time) - - -# Checks if the path is connected, returns a circle if it finds one and the RESULT -def is_connected(resx) -> bool: - - - 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. - resx = resx[:L*L] - - # first round the results to have only 0-1 values - for i, elem in enumerate(resx): - resx[i] = round(elem) - - 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 - - remaining = edges - remaining.remove(edge1) - - 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 - 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 - - vertices_visited.append(edge1[1]) - - - if len(vertices_visited) == n_edges +1 : - return vertices_visited, [] - else: - return vertices_visited, edges_visited - - -# 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 - 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 - - - -# 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 - for i in range(L) : - h = zeros*i + ones + zeros*(L-1-i) + [0]*(L-1) - A_ub.append(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.append(ones*L + [0]*(L-1)) - b_ub.append(max_landmarks+1) - - return A_ub, b_ub - - - -def solve_optimizationv3(landmarks, max_walking_time): - L = len(landmarks) - - # 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 = parameters['detour factor'] - speed = parameters['average walking speed'] - - # Create distance matrix - A = np.zeros((L, L)) - for i in range(L): - for j in range(L): - if i != j: - A[i, j] = get_time(landmarks[i].location, landmarks[j].location, detour, speed) - - # Define the linear program - c = np.hstack((A.flatten(), [0]*(L-1))) - bounds = [(0, 1) for _ in range(L*L+L-1)] - - # Flow conservation constraints - A_eq = [] - b_eq = [] - - # Each node (except start and end) has one incoming and one outgoing edge - for i in range(L): - if i == 0 or i == L-1: - continue - A_eq.append([1 if j // L == i else 0 for j in range(L*L)] + [0]*(L-1)) - b_eq.append(1) - A_eq.append([1 if j % L == i else 0 for j in range(L*L)] + [0]*(L-1)) - b_eq.append(1) - - # Start node constraint - A_eq.append([1 if j // L == 0 else 0 for j in range(L*L)] + [0]*(L-1)) - b_eq.append(1) - - # End node constraint - A_eq.append([1 if j % L == L-1 else 0 for j in range(L*L)] + [0]*(L-1)) - b_eq.append(1) - - # Subtour elimination constraints - A_ub = [] - b_ub = [] - - # u_i - u_j + L*x_ij <= L-1 for all i != j - for i in range(1, L): - for j in range(1, L): - if i != j: - constraint = [0] * (L * L + L - 1) - constraint[i * L + j] = L - constraint[j * L + i] = -L - A_ub.append(constraint) - b_ub.append(L - 1) - - - A_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks). - - # Convert constraints to numpy arrays - A_eq = np.array(A_eq) - A_ub = np.array(A_ub) - b_ub = np.array(b_ub) - b_eq = np.array(b_eq) - - # Solve the linear program - result = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method='highs') - - if result.success: - x = result.x[:L*L].reshape((L, L)) - path = [] - for i in range(L): - for j in range(L): - if x[i, j] > 0.5: - path.append((i, j)) - print(f"({i}, {j})") - - order, _ = is_connected(result.x) - L, _ = link_list(order, landmarks) - - print_res(L, len(landmarks)) - print("\nTotal score : " + str(int(-result.fun))) - - return L - else: - print("no results") - return [] - - - -# Main optimization pipeline -# def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_details: bool) : - -# 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_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks). -# #A_ub, b_ub = break_sym(L, A_ub, b_ub) # break the 'zig-zag' symmetry -# #A_ub, b_ub = prevent_subtours(L, A_ub, b_ub) - -# # SET CONSTRAINTS FOR EQUALITY -# #A_eq, b_eq = init_eq_not_stay(L) # Force solution not to stay in same place -# #A_eq, b_eq = 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(L, A_eq, b_eq) # Force start and finish positions -# #A_eq, b_eq = respect_order(L, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor) - -# # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) -# x_bounds = [(0, 1)]*(L*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, circle = is_connected(res.x) -# i = 0 -# 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) -# if not res.success : -# raise ArithmeticError(f"No solution found after {timeout} iterations.") - -# order, circle = is_connected(res.x) -# if len(circle) == 0 : -# 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, total_dist = 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, len(landmarks)) -# print("\nTotal score : " + str(int(-res.fun))) - -# return L - - - - - - diff --git a/backend/src/optimizer_v4.py b/backend/src/optimizer_v4.py index d8edea6..a98492c 100644 --- a/backend/src/optimizer_v4.py +++ b/backend/src/optimizer_v4.py @@ -11,12 +11,9 @@ from structs.landmarks import Landmark # Function to print the result -def print_res(L: List[Landmark], L_tot): +def print_res(L: List[Landmark]): - if len(L) == L_tot: - print('\nAll landmarks can be visited within max_steps, the following order is suggested : ') - else : - print('Could not visit all the landmarks, the following order is suggested : ') + print('The following order is suggested : ') dist = 0 for elem in L : @@ -27,7 +24,7 @@ def print_res(L: List[Landmark], L_tot): print('- ' + elem.name) print("\nMinutes walked : " + str(dist)) - print(f"Visited {len(L)-2} out of {L_tot-2} landmarks") + print(f"Visited {len(L)-2} landmarks") # Prevent the use of a particular solution @@ -178,7 +175,7 @@ def init_ub_dist(landmarks: List[Landmark], max_steps: int): closest = sorted(dist_table)[:22] for i, dist in enumerate(dist_table) : if dist not in closest : - dist_table[i] = 10000000 + dist_table[i] = 32700 A_ub += dist_table c = c*len(landmarks) @@ -243,7 +240,7 @@ def init_eq_not_stay(L: int): if j == i : l[j + i*L] = 1 - l = np.array(np.array(l)) + l = np.array(np.array(l), dtype=np.int8) return [l], [0] @@ -374,23 +371,23 @@ def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_deta # 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)) + 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)) + 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)) + 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)) + 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)) + 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) @@ -419,6 +416,9 @@ def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_deta 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 : @@ -435,7 +435,7 @@ def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_deta if printing_details is True : if i != 0 : print(f"Neded to recompute paths {i} times because of unconnected loops...") - print_res(L, len(landmarks)) + print_res(L) print("\nTotal score : " + str(int(-res.fun))) return L diff --git a/backend/src/parameters/landmarks_manager.params b/backend/src/parameters/landmarks_manager.params index 54638b5..9b5a170 100644 --- a/backend/src/parameters/landmarks_manager.params +++ b/backend/src/parameters/landmarks_manager.params @@ -1,5 +1,5 @@ { - "city bbox side" : 10, + "city bbox side" : 3, "radius close to" : 50, "church coeff" : 0.9, "park coeff" : 1.2, diff --git a/backend/src/refiner.py b/backend/src/refiner.py index 1e3685d..1971170 100644 --- a/backend/src/refiner.py +++ b/backend/src/refiner.py @@ -47,7 +47,7 @@ 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)) -# Rearrange some landmarks in the order of visit +# Rearrange some landmarks in the order of visit to group visit def rearrange(landmarks: List[Landmark]) -> List[Landmark]: i = 1 @@ -171,45 +171,58 @@ def fix_using_polygon(tour: List[Landmark])-> List[Landmark] : # Append the finish back and correct the time to reach better_tour.append(tour[-1]) - # Rearrange only if polygon - better_tour = rearrange(better_tour) + # Rearrange only if polygon still not simple + if not better_tour_poly.is_simple : + better_tour = rearrange(better_tour) return better_tour # 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, print_infos: bool) -> List[Landmark] : +def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, detour: 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'] + 4 - minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200) + # No need to refine if no detour is taken + # if detour == 0 : + if False : + new_tour = base_tour + + else : + minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200) - if print_infos : print("Using " + 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 + # 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 tour - new_tour = solve_optimization(full_set, max_time, False, max_landmarks) + # get a new tour + new_tour = solve_optimization(full_set, max_time+detour, False, max_landmarks) + if new_tour is None : + new_tour = base_tour + + # Link the new tour new_tour, new_dist = link_list_simple(new_tour) - # if the tour contains only one landmark, return + # If the tour contains only one landmark, return if len(new_tour) < 4 : 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) + # 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 : better_tour = fix_using_polygon(better_tour) - + # Link the tour again better_tour, better_dist = link_list_simple(better_tour) + # Choose the better tour depending on walked distance if new_dist < better_dist : final_tour = new_tour else : @@ -217,7 +230,7 @@ def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], ma if print_infos : print("\n\n\nRefined tour (result of second stage optimization): ") - print_res(final_tour, len(full_set)) + print_res(final_tour) total_score = 0 for elem in final_tour : total_score += elem.attractiveness diff --git a/backend/src/refiner_v2.py b/backend/src/refiner_v2.py deleted file mode 100644 index 236217f..0000000 --- a/backend/src/refiner_v2.py +++ /dev/null @@ -1,306 +0,0 @@ -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, Tuple -from scipy.spatial import KDTree -from math import pi -import networkx as nx - -from structs.landmarks import Landmark -from landmarks_manager import take_most_important -from optimizer_v4 import solve_optimization, link_list_simple, print_res, get_time -from optimizer_v2 import generate_path, generate_path2 - - -def create_corridor(landmarks: List[Landmark], width: float) : - - corrected_width = (180*width)/(6371000*pi) - - path = create_linestring(landmarks) - obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2) - - return obj - - -def create_linestring(landmarks: List[Landmark])->List[Point] : - - points = [] - - for landmark in landmarks : - points.append(Point(landmark.location)) - - return LineString(points) - - -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_through_all_landmarks(landmarks: List[Landmark]) -> Tuple[List[Landmark], Polygon]: - - # 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_time(current_landmark.location, lm.location, detour, speed)) - 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 = [] - visited_names = [] - area = create_corridor(visited_landmarks, width) - - for visited in visited_landmarks : - visited_names.append(visited.name) - - for landmark in all_landmarks : - 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, len(visited_landmarks)) - - - -"""def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] : - - 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") - - 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_tour = solve_optimization(full_set, max_time, print_infos) - - return new_tour""" - - - -def fix_using_polygon(tour: List[Landmark])-> List[Landmark] : - - coords = [] - coords_dict = {} - for landmark in tour : - coords.append(landmark.location) - if landmark.name != 'finish' : - coords_dict[landmark.location] = landmark - - tour_poly = Polygon(coords) - - better_tour_poly = tour_poly.buffer(0) - try : - xs, ys = better_tour_poly.exterior.xy - - if len(xs) != len(tour) : - 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 - - except : - 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]) : - y = ys[i] - better_tour.append(coords_dict[tuple((x,y))]) - name_index[coords_dict[tuple((x,y))].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(tour[-1]) - - # Rearrange only if polygon - better_tour = rearrange(better_tour) - - return better_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'] + 4 - - minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200) - - 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 tour - new_tour = solve_optimization(full_set, max_time, False, max_landmarks) - new_tour, new_dist = link_list_simple(new_tour) - - better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour) - - if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid : - better_tour = fix_using_polygon(better_tour) - - - better_tour, better_dist = link_list_simple(better_tour) - - if new_dist < better_dist : - final_tour = new_tour - else : - final_tour = better_tour - - if print_infos : - print("\n\n\nRefined tour (result of second stage optimization): ") - print_res(final_tour, len(full_set)) - total_score = 0 - for elem in final_tour : - total_score += elem.attractiveness - - print("\nTotal score : " + str(total_score)) - - - - return final_tour - - - -def refine_path(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] : - - print("\nRefining the base tour...") - # 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'] + 3 - - """if len(base_tour)-2 >= max_landmarks : - return base_tour""" - - minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200) - - if print_infos : print("Using " + str(len(minor_landmarks)) + " minor landmarks around the predicted path") - - full_set = base_tour + minor_landmarks # create full set of possible landmarks - - print("\nRefined tour (result of second stage optimization): ") - - new_path = generate_path2(full_set, max_time, max_landmarks) - - return new_path - - - - -# If a tour is not connected -def correct_path(tour: 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 = parameters['detour factor'] - speed = parameters['average walking speed'] - - G = nx.Graph() - - coords = [] - landmap = {} - for i, landmark in enumerate(tour) : - coords.append(landmark.location) - landmap[i] = landmark - G.add_node(i, pos=landmark.location, weight=landmark.attractiveness) - - kdtree = KDTree(coords) - - k = 3 - for node, coord in coords: - indices = kdtree.query(coord, k + 1)[1] # k+1 because the closest neighbor is the node itself - for idx in indices[1:]: # skip the first one (itself) - neighbor = list(coords)[idx] - distance = get_time(coord, coords[neighbor], detour, speed) - G.add_edge(node, neighbor, weight=distance) - - path = nx.approximation.traveling_salesman_problem(G, weight='weight', cycle=True) - - if len(path) != len(tour) : - print("nope") - - lis = [landmap[id] for id in path] - - lis, tot_dist = link_list_simple(lis) - - print_res(lis, len(tour)) - - return path - - diff --git a/backend/src/tester.py b/backend/src/tester.py index 1b00985..5f2bf7b 100644 --- a/backend/src/tester.py +++ b/backend/src/tester.py @@ -31,7 +31,7 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]: sightseeing=Preference( name='sightseeing', type=LandmarkType(landmark_type='sightseeing'), - score = 0), + score = 5), nature=Preference( name='nature', type=LandmarkType(landmark_type='nature'), @@ -58,21 +58,22 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]: landmarks_short.insert(0, start) landmarks_short.append(finish) - max_walking_time = 50 # minutes + max_walking_time = 480 # minutes detour = 0 # minutes # First stage optimization base_tour = solve_optimization(landmarks_short, max_walking_time, True) # Second stage using linear optimization - if detour != 0 or len(base_tour) <= 4: - refined_tour = refine_optimization(landmarks, base_tour, max_walking_time+detour, True) + + refined_tour = refine_optimization(landmarks, base_tour, max_walking_time, detour, True) return refined_tour -test4(tuple((48.8344400, 2.3220540))) # Café Chez César +#test4(tuple((48.8344400, 2.3220540))) # Café Chez César #test4(tuple((48.8375946, 2.2949904))) # Point random #test4(tuple((47.377859, 8.540585))) # Zurich HB -#test4(tuple((45.7576485, 4.8330241))) # Lyon Bellecour \ No newline at end of file +test4(tuple((45.7576485, 4.8330241))) # Lyon Bellecour +#test4(tuple((48.5848435, 7.7332974))) # Strasbourg Gare \ No newline at end of file