From 568e7bfbc46132bc167c767d4c270bbe58c85591 Mon Sep 17 00:00:00 2001 From: Helldragon67 <kilian.scheidecker@orange.fr> Date: Mon, 8 Jul 2024 01:20:17 +0200 Subject: [PATCH] upgraded optimizer --- backend/src/optimizer_v4.py | 237 +++++++++++++++++++++++++++++------- backend/src/refiner.py | 5 + backend/src/tester.py | 26 +--- 3 files changed, 201 insertions(+), 67 deletions(-) diff --git a/backend/src/optimizer_v4.py b/backend/src/optimizer_v4.py index b3c09ee..0a5d68c 100644 --- a/backend/src/optimizer_v4.py +++ b/backend/src/optimizer_v4.py @@ -3,7 +3,7 @@ import json, os from typing import List, Tuple from scipy.optimize import linprog -from math import radians, sin, cos, acos +from collections import defaultdict, deque from geopy.distance import geodesic from shapely import Polygon @@ -31,7 +31,7 @@ def print_res(L: List[Landmark], L_tot): # Prevent the use of a particular solution -def prevent_config(resx, A_ub, b_ub) -> bool: +def prevent_config(resx, A_ub, b_ub): for i, elem in enumerate(resx): resx[i] = round(elem) @@ -58,8 +58,31 @@ def prevent_config(resx, A_ub, b_ub) -> bool: return A_ub, b_ub +def prevent_circle(circle_vertices: list, L: int, A_eq: list, b_eq: list) : + + l1 = [0]*L*L + l2 = [0]*L*L + for i, node in enumerate(circle_vertices[:-1]) : + next = circle_vertices[i+1] + + l1[node*L + next] = 1 + l2[next*L + node] = 1 + + s = circle_vertices[0] + g = circle_vertices[-1] + + l1[g*L + s] = 1 + l2[s*L + g] = 1 + + A_eq = np.vstack((A_eq, l1)) + b_eq.append(0) + A_eq = np.vstack((A_eq, l2)) + b_eq.append(0) + + return A_eq, b_eq + # Prevent the possibility of a given solution bit -def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool: +def break_circle(circle_vertices: list, L: int, A_ub: list, b_ub: list): if L-1 in circle_vertices : circle_vertices.remove(L-1) @@ -74,9 +97,26 @@ def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool: return A_ub, b_ub +""" +def break_circles(circle_edges_list: list, L: int, A_ub: list, b_ub: list): + + for circle_edges in circle_edges_list : + if L-1 in circle_vertices : + circle_vertices.remove(L-1) + + h = [0]*L*L + for i in range(L) : + if i in circle_vertices : + h[i*L:i*L+L] = [1]*L + + 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, returns a circle if it finds one and the RESULT -def is_connected(resx) -> bool: +def is_connected(resx): # first round the results to have only 0-1 values for i, elem in enumerate(resx): @@ -97,13 +137,15 @@ def is_connected(resx) -> bool: 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 + edge1 = (ind_a[0], ind_b[0]) + del ind_a[0] + + edges_visited.append(edge1) + vertices_visited.append(edge1[0]) + remaining = edges remaining.remove(edge1) @@ -111,27 +153,135 @@ def is_connected(resx) -> bool: 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) + """if edge1[1] in vertices_visited : + ind_b.remove(edge2[1]) + #edges_visited.append(edge2) break_flag = True break - else : - vertices_visited.append(edge1[1]) - edges_visited.append(edge2) - remaining.remove(edge2) - edge1 = edge2 + else : """ + vertices_visited.append(edge1[1]) + ind_a.remove(edge2[0]) + ind_b.remove(edge2[0]) + #edges_visited.append(edge2) + remaining.remove(edge2) + edge1 = edge2 elif edge1[1] == L-1 or edge1[1] in vertices_visited: - break_flag = True - break + ind_b.remove(edge1[1]) + break_flag = True + break vertices_visited.append(edge1[1]) - + # Return order of visit if all good if len(vertices_visited) == n_edges +1 : return vertices_visited, [] - else: - return vertices_visited, edges_visited + + """edge1 = (ind_a[0], ind_b[0]) + + vertices_visited.clear() + edges_visited.clear() + + edges_visited.append(edge1) + vertices_visited.append(edge1[0]) + + 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]) + """ + + return vertices_visited, edges_visited + + +def is_connected2(resx) : + + # first round the results to have only 0-1 values + for i, elem in enumerate(resx): + resx[i] = round(elem) + + N = len(resx) # length of res + L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def. + n_edges = resx.sum() # number of edges + + nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0] + + nonzero_tup = np.unravel_index(nonzeroind, (L,L)) + + ind_a = nonzero_tup[0].tolist() + ind_b = nonzero_tup[1].tolist() + + # print(f"ind_a = {ind_a}") + # print(f"ind_b = {ind_b}") + + # Step 1: Create a graph representation + graph = defaultdict(list) + for a, b in zip(ind_a, ind_b): + graph[a].append(b) + + # Step 2: Function to perform BFS/DFS to extract journeys + def get_journey(start): + journey_nodes = [] + #journey_edges = [] + visited = set() + stack = deque([start]) + + while stack: + node = stack.pop() + if node not in visited: + visited.add(node) + journey_nodes.append(node) + for neighbor in graph[node]: + #journey_edges.append((node, neighbor)) + if neighbor not in visited: + stack.append(neighbor) + + return journey_nodes#, journey_edges + + # Step 3: Extract all journeys + all_journeys_nodes = [] + #all_journeys_edges = [] + visited_nodes = set() + + for node in ind_a: + if node not in visited_nodes: + journey_nodes = get_journey(node) + all_journeys_nodes.append(journey_nodes) + #all_journeys_edges.append(journey_edges) + visited_nodes.update(journey_nodes) + + + + for l in all_journeys_nodes : + if 0 in l : + order = l + all_journeys_nodes.remove(l) + break + + if len(all_journeys_nodes) == 0 : + return order, None + + return order, all_journeys_nodes + + # Function that returns the distance in meters from one location to another @@ -251,37 +401,30 @@ def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) : for i, elem in enumerate(landmarks) : if elem.must_do is True and elem.name not in ['finish', 'start']: l = [0]*L*L - for j in range(L) : # sets the horizontal ones (go from) - l[j +i*L] = 1 # sets the vertical ones (go to) double check if good - - for k in range(L-1) : - l[k*L+L-1] = 1 + l[i*L:i*L+L] = [1]*L # set mandatory departures from landmarks tagged as 'must_do' A_eq = np.vstack((A_eq,l)) - b_eq.append(2) + b_eq.append(1) return A_eq, b_eq # Constraint to ensure start at start and finish at goal def respect_start_finish(L: int, A_eq: list, b_eq: list): - ls = [1]*L + [0]*L*(L-1) # sets only horizontal ones for start (go from) - ljump = [0]*L*L - ljump[L-1] = 1 # Prevent start finish jump - lg = [0]*L*L - ll = [0]*L*(L-1) + [1]*L - for k in range(L-1) : # sets only vertical ones for goal (go to) - ll[k*L] = 1 - if k != 0 : # Prevent the shortcut start -> finish - lg[k*L+L-1] = 1 + l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones) + l_start[L-1] = 0 # prevents the jump from start to finish + l_goal = [0]*L*L # sets arrivals only for finish (vertical ones) + l_L = [0]*L*(L-1) + [1]*L # prevents arrivals at start and departures from goal + for k in range(L-1) : # sets only vertical ones for goal (go to) + l_L[k*L] = 1 + if k != 0 : + l_goal[k*L+L-1] = 1 - A_eq = np.vstack((A_eq,ls)) - A_eq = np.vstack((A_eq,ljump)) - A_eq = np.vstack((A_eq,lg)) - A_eq = np.vstack((A_eq,ll)) + A_eq = np.vstack((A_eq,l_start)) + A_eq = np.vstack((A_eq,l_goal)) + A_eq = np.vstack((A_eq,l_L)) b_eq.append(1) - b_eq.append(0) b_eq.append(1) b_eq.append(0) @@ -393,15 +536,19 @@ def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_deta # If there is a solution, we're good to go, just check for connectiveness else : - order, circle = is_connected(res.x) + order, circles = is_connected2(res.x) + #nodes, edges = is_connected2(res.x) i = 0 timeout = 80 - while len(circle) != 0 and i < timeout: + while circles is not None 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) + #A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub) + for circle in circles : + A_ub, b_ub = prevent_circle(circle, 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 : + order, circles = is_connected2(res.x) + #nodes, edges = is_connected2(res.x) + if circles is None : break print(i) i += 1 diff --git a/backend/src/refiner.py b/backend/src/refiner.py index 9d18383..1e3685d 100644 --- a/backend/src/refiner.py +++ b/backend/src/refiner.py @@ -196,7 +196,12 @@ def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], ma # get a new tour new_tour = solve_optimization(full_set, max_time, False, max_landmarks) new_tour, new_dist = link_list_simple(new_tour) + + # if the tour contains only one landmark, return + if len(new_tour) < 4 : + return new_tour + # find shortest path using the nearest neighbor heuristic 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 : diff --git a/backend/src/tester.py b/backend/src/tester.py index eef098a..eb7d36a 100644 --- a/backend/src/tester.py +++ b/backend/src/tester.py @@ -7,8 +7,7 @@ from landmarks_manager import generate_landmarks from fastapi.encoders import jsonable_encoder from optimizer_v4 import solve_optimization -from optimizer_v2 import generate_path -from refiner import refine_optimization, refine_path +from refiner import refine_optimization from structs.landmarks import Landmark from structs.landmarktype import LandmarkType from structs.preferences import Preferences, Preference @@ -81,7 +80,7 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]: shopping=Preference( name='shopping', type=LandmarkType(landmark_type='shopping'), - score = 5)) + score = 0)) # Create start and finish @@ -97,37 +96,20 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]: #write_data(landmarks, "landmarks_Lyon.txt") # Insert start and finish to the landmarks list - #landmarks_short = landmarks_short[:4] landmarks_short.insert(0, start) landmarks_short.append(finish) - # TODO use these parameters in another way - 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'] max_walking_time = 120 # minutes - detour = 10 # minutes - + detour = 30 # minutes # First stage optimization base_tour = solve_optimization(landmarks_short, max_walking_time, True) - - #base_tour = solve_optimization(landmarks_short, max_walking_time, True) - - # First stage using NetworkX - #base_tour = generate_path(landmarks_short, max_walking_time, max_landmarks) - # Second stage using linear optimization - if detour != 0 : + if detour != 0 or len(base_tour) <= 4: refined_tour = refine_optimization(landmarks, base_tour, max_walking_time+detour, True) - # Second stage using NetworkX - #refined_tour = refine_path(landmarks, base_tour, max_walking_time+detour, True) - # Use NetworkX again to correct to shortest path - #refined_tour = refine_path(landmarks, base_tour, max_walking_time+detour, True) - return refined_tour