diff --git a/backend/src/__pycache__/optimizer.cpython-310.pyc b/backend/src/__pycache__/optimizer.cpython-310.pyc new file mode 100644 index 0000000..2446c04 Binary files /dev/null and b/backend/src/__pycache__/optimizer.cpython-310.pyc differ diff --git a/backend/src/main.py b/backend/src/main.py index e0072b0..772daca 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,23 +1,26 @@ -import fastapi -from dataclasses import dataclass +from optimizer import solve_optimization +from optimizer import landmark + +def main(): + + # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS + max_steps = 16 -@dataclass -class Destination: - name: str - location: tuple - attractiveness: int + # Initialize all landmarks (+ start and goal). Order matters here + landmarks = [] + landmarks.append(landmark("départ", -1, (0, 0))) + landmarks.append(landmark("tour eiffel", 99, (0,2))) # PUT IN JSON + landmarks.append(landmark("arc de triomphe", 99, (0,4))) + landmarks.append(landmark("louvre", 99, (0,6))) + landmarks.append(landmark("montmartre", 99, (0,10))) + landmarks.append(landmark("concorde", 99, (0,8))) + landmarks.append(landmark("arrivée", -1, (0, 0))) + + + visiting_order = solve_optimization(landmarks, max_steps, True) -d = Destination() - - - -def get_route() -> list[Destination]: - return {"route": "Hello World"} - -endpoint = ("/get_route", get_route) -end if __name__ == "__main__": - fastapi.run() + main() \ No newline at end of file diff --git a/backend/src/main_example.py b/backend/src/main_example.py new file mode 100644 index 0000000..e0072b0 --- /dev/null +++ b/backend/src/main_example.py @@ -0,0 +1,23 @@ +import fastapi +from dataclasses import dataclass + + +@dataclass +class Destination: + name: str + location: tuple + attractiveness: int + + + +d = Destination() + + + +def get_route() -> list[Destination]: + return {"route": "Hello World"} + +endpoint = ("/get_route", get_route) +end +if __name__ == "__main__": + fastapi.run() diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py index aaf214b..66efaeb 100644 --- a/backend/src/optimizer.py +++ b/backend/src/optimizer.py @@ -11,9 +11,8 @@ class landmark : self.loc = loc - - -def untangle2(resx: list) : +# Convert the solution of the optimization into the list of edges to follow. Order is taken into account +def untangle(resx: list) : 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 @@ -40,65 +39,31 @@ def untangle2(resx: list) : return order -# Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks -def untangle(resx: list) : - 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_landmarks = resx.sum() # number of edges - visit_order = [] - cnt = 0 - - if n_landmarks % 2 == 1 : # if odd number of visited checkpoints - for i in range(L) : - for j in range(L) : - if res[i*L + j] == 1 : # if index is 1 - cnt += 1 # increment counter - if cnt % 2 == 1 : # if counter odd - visit_order.append(i) - visit_order.append(j) - else : # if even number of ones - for i in range(L) : - for j in range(L) : - if res[i*L + j] == 1 : # if index is one - cnt += 1 # increment counter - if j % (L-1) == 0 : # if last node - visit_order.append(j) # append only the last index - return visit_order # return - if cnt % 2 == 1 : - visit_order.append(i) - visit_order.append(j) - return visit_order - # Just to print the result -def print_res(res: list, landmarks: list, P) : +def print_res(res, landmarks: list, P) : X = abs(res.x) + order = untangle(X) - N = int(np.sqrt(len(X))) + """N = int(np.sqrt(len(X))) for i in range(N): print(X[i*N:i*N+N]) - - order = untangle2(X) - - order_ideal = [0, 0, 0, 0, 0, 0, 1, 0] - - # print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum - # print("Optimal point:", res.x) - - #for i,x in enumerate(X) : X[i] = round(x,0) - - #print(order) + print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum + print("Optimal point:", res.x) + for i,x in enumerate(X) : X[i] = round(x,0) + print(order)""" if (X.sum()+1)**2 == len(X) : - print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest') + 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 could be the fastest but not sure') - print("Order of visit :") + print('Could not visit all the landmarks, the following order is suggested : ') + for idx in order : print('- ' + landmarks[idx].name) steps = path_length(P, abs(res.x)) print("\nSteps walked : " + str(steps)) + return order # Checks for cases of circular symmetry in the result def has_circle(resx: list) : @@ -164,8 +129,8 @@ def break_sym(landmarks, A_ub, b_ub): return A_ub, b_ub - -def prevent_circle(landmarks, A_ub, b_ub, circle) : +# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops +def break_circle(landmarks, A_ub, b_ub, circle) : N = len(landmarks) l = [0]*N*N @@ -195,7 +160,7 @@ def respect_number(landmarks, A_ub, b_ub): print("\n")""" return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks) -# Constraint to tie the problem together and have a connected path +# Constraint to tie the problem together. Necessary but not sufficient to avoid circles def respect_order(landmarks: list, A_eq, b_eq): N = len(landmarks) for i in range(N-1) : # Prevent stacked ones @@ -294,68 +259,62 @@ def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) : def path_length(P: list, resx: list) : return np.dot(P, resx) -# Initialize all landmarks (+ start and goal). Order matters here -landmarks = [] -landmarks.append(landmark("départ", -1, (0, 0))) -landmarks.append(landmark("tour eiffel", 99, (0,2))) # PUT IN JSON -landmarks.append(landmark("arc de triomphe", 99, (0,4))) -landmarks.append(landmark("louvre", 99, (0,6))) -landmarks.append(landmark("montmartre", 99, (0,10))) -landmarks.append(landmark("concorde", 99, (0,8))) -landmarks.append(landmark("arrivée", -1, (0, 0))) +# Main optimization pipeline +def solve_optimization (landmarks, max_steps, printing) : + # SET CONSTRAINTS FOR INEQUALITY + c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other + P = A_ub # store the paths for later. Needed to compute path length + A_ub, b_ub = respect_number(landmarks, A_ub, b_ub) # Respect max number of visits. + # TODO : Problems with circular symmetry + A_ub, b_ub = break_sym(landmarks, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values -# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS -max_steps = 16 + # SET CONSTRAINTS FOR EQUALITY + A_eq, b_eq = init_eq_not_stay(landmarks) # Force solution not to stay in same place + A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal -# SET CONSTRAINTS FOR INEQUALITY -c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other -P = A_ub # store the paths for later. Needed to compute path length -A_ub, b_ub = respect_number(landmarks, A_ub, b_ub) # Respect max number of visits. + A_eq, b_eq = respect_order(landmarks, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor) -# TODO : Problems with circular symmetry -A_ub, b_ub = break_sym(landmarks, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values + # Bounds for variables (x can only be 0 or 1) + x_bounds = [(0, 1)] * len(c) -# SET CONSTRAINTS FOR EQUALITY -A_eq, b_eq = init_eq_not_stay(landmarks) # Force solution not to stay in same place -A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal + # Solve linear programming problem -A_eq, b_eq = respect_order(landmarks, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor) - -# Bounds for variables (x can only be 0 or 1) -x_bounds = [(0, 1)] * len(c) - -# 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) -circle = has_circle(res.x) - -while len(circle) != 0 : - print("The solution has a circular path. Not interpretable.") - print("Need to add constraints until no circle ") - - A_ub, b_ub = prevent_circle(landmarks, A_ub, b_ub, circle) 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) circle = has_circle(res.x) + i = 0 + + # Break the circular symmetry if needed + while len(circle) != 0 : + A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle) + 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) + circle = has_circle(res.x) + i += 1 + # Raise error if no solution is found + if not res.success : -# Raise error if no solution is found -if not res.success : - print(f"No solution has been found within given timeframe.\nMinimum steps to visit all must_see is : {H}") - # Override the max_steps using the heuristic - for i, val in enumerate(b_ub) : - if val == max_steps : b_ub[i] = H + # Override the max_steps using the heuristic + for i, val in enumerate(b_ub) : + if val == max_steps : b_ub[i] = H - # Solve problem again : - 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) - - -# Print result -print_res(res, landmarks, P) + # Solve problem again : + 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 ValueError("No solution could be found, even when increasing max_steps using the heuristic") + + if printing is True : + if i != 0 : + print(f"Neded to recompute paths {i} times because of unconnected loops...") + X = print_res(res, landmarks, P) + return X + + else : + return untangle(res.x)