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