From 2b31ce5f6b057c985bc22caa97d5d299a5e8392c Mon Sep 17 00:00:00 2001 From: Kilian Scheidecker Date: Wed, 22 May 2024 10:16:33 +0200 Subject: [PATCH] Cleanup and created main --- .../src/__pycache__/optimizer.cpython-310.pyc | Bin 0 -> 6065 bytes backend/src/main.py | 37 +++-- backend/src/main_example.py | 23 +++ backend/src/optimizer.py | 155 +++++++----------- 4 files changed, 100 insertions(+), 115 deletions(-) create mode 100644 backend/src/__pycache__/optimizer.cpython-310.pyc create mode 100644 backend/src/main_example.py diff --git a/backend/src/__pycache__/optimizer.cpython-310.pyc b/backend/src/__pycache__/optimizer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2446c047e04e4aa4cec9c2a5628a2c89ec72a9c7 GIT binary patch literal 6065 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 F{s}4|`=0;+ literal 0 HcmV?d00001 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)