backend/new-overpass #52
| @@ -26,3 +26,4 @@ fastapi-cli = "*" | ||||
| scikit-learn = "*" | ||||
| pyqt6 = "*" | ||||
| loki-logger-handler = "*" | ||||
| pulp = "*" | ||||
|   | ||||
							
								
								
									
										1555
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1555
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -11,7 +11,7 @@ def client(): | ||||
|     """Client used to call the app.""" | ||||
|     return TestClient(app) | ||||
|  | ||||
|  | ||||
| ''' | ||||
| def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|     """ | ||||
|     Test n°1 : Custom test in Turckheim to ensure small villages are also supported. | ||||
| @@ -21,7 +21,7 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|         request: | ||||
|     """ | ||||
|     start_time = time.time()  # Start timer | ||||
|     duration_minutes = 15 | ||||
|     duration_minutes = 20 | ||||
|  | ||||
|     response = client.post( | ||||
|         "/trip/new", | ||||
| @@ -45,8 +45,8 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|     # Add details to report | ||||
|     log_trip_details(request, landmarks, result['total_time'], duration_minutes) | ||||
|  | ||||
|     # for elem in landmarks : | ||||
|     #     print(elem) | ||||
|     for elem in landmarks : | ||||
|         print(elem) | ||||
|  | ||||
|     # checks : | ||||
|     assert response.status_code == 200  # check for successful planning | ||||
| @@ -56,6 +56,8 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|     assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" | ||||
|     assert 2==3 | ||||
| ''' | ||||
|  | ||||
|  | ||||
| def test_bellecour(client, request) :   # pylint: disable=redefined-outer-name | ||||
|     """ | ||||
|     Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area. | ||||
| @@ -87,16 +89,16 @@ def test_bellecour(client, request) :   # pylint: disable=redefined-outer-name | ||||
|     # Add details to report | ||||
|     log_trip_details(request, landmarks, result['total_time'], duration_minutes) | ||||
|  | ||||
|     # for elem in landmarks : | ||||
|     #     print(elem) | ||||
|     for elem in landmarks : | ||||
|         print(elem) | ||||
|  | ||||
|     # checks : | ||||
|     assert response.status_code == 200  # check for successful planning | ||||
|     assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" | ||||
|     assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 | ||||
|     # assert 2 == 3 | ||||
|  | ||||
|     assert 2 == 3 | ||||
|  | ||||
| ''' | ||||
| def test_Paris(client, request) :   # pylint: disable=redefined-outer-name | ||||
|     """ | ||||
|     Test n°2 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area. | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import yaml, logging | ||||
| import numpy as np | ||||
|  | ||||
| import pulp as pl | ||||
| from scipy.optimize import linprog | ||||
| from collections import defaultdict, deque | ||||
|  | ||||
| @@ -9,6 +9,9 @@ from .get_time_separation import get_time | ||||
| from ..constants import OPTIMIZER_PARAMETERS_PATH | ||||
|  | ||||
|  | ||||
| logging.getLogger('pulp').setLevel(level=logging.CRITICAL) | ||||
|  | ||||
|  | ||||
| class Optimizer: | ||||
|  | ||||
|     logger = logging.getLogger(__name__) | ||||
| @@ -62,7 +65,7 @@ class Optimizer: | ||||
|         b_ub[0] = round(max_time*self.overshoot) | ||||
|  | ||||
|         for i, spot1 in enumerate(landmarks) : | ||||
|             c[i] = -spot1.attractiveness | ||||
|             c[i] = spot1.attractiveness | ||||
|             for j in range(i+1, L) : | ||||
|                 if i !=j : | ||||
|                     t = get_time(spot1.location, landmarks[j].location) | ||||
| @@ -102,8 +105,6 @@ class Optimizer: | ||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||
|         """ | ||||
|         # First constraint: each landmark is visited exactly once | ||||
|         # A = np.zeros((L-1, L*L), dtype=np.int8) | ||||
|         # b = [] | ||||
|         # Fill-in row 2 until row L-2 | ||||
|         for i in range(1, L-1): | ||||
|             A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16) | ||||
| @@ -129,12 +130,10 @@ class Optimizer: | ||||
|             tuple[np.ndarray, list[int]]:   Inequality constraint coefficients and  | ||||
|                                             the right-hand side of the inequality constraints. | ||||
|         """ | ||||
|         # b = [] | ||||
|         upper_ind = np.triu_indices(L,0,L) | ||||
|         up_ind_x = upper_ind[0] | ||||
|         up_ind_y = upper_ind[1] | ||||
|  | ||||
|         # A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) | ||||
|         # Fill-in rows L to 2*L-1 | ||||
|         incr = 0 | ||||
|         for i in range(int((L*L+L)/2)) : | ||||
| @@ -144,14 +143,12 @@ class Optimizer: | ||||
|                 b[L+incr] = 1 | ||||
|                 incr += 1 | ||||
|  | ||||
|         # return A[~np.all(A == 0, axis=1)], b | ||||
|  | ||||
|  | ||||
|     def init_eq_not_stay(self, landmarks: list):  | ||||
|         """ | ||||
|         Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.). | ||||
|         -> Adds 1 row of constraints | ||||
|         -> Pre-allocates A_eq for the rest of the computations with (L+2 + dynamic incr) rows | ||||
|         -> Pre-allocates A_eq for the rest of the computations with (L+ 2 + dynamic incr) rows | ||||
|  | ||||
|         Args: | ||||
|             L (int): Number of landmarks. | ||||
| @@ -168,7 +165,7 @@ class Optimizer: | ||||
|         A_eq = np.zeros((L+2+incr, L*L), dtype=np.int8) | ||||
|         b_eq = np.zeros(L+2+incr, dtype=np.int8) | ||||
|         l = np.zeros((L, L), dtype=np.int8) | ||||
|          | ||||
|  | ||||
|         # Set diagonal elements to 1 (to prevent staying in the same position) | ||||
|         np.fill_diagonal(l, 1) | ||||
|  | ||||
| @@ -220,7 +217,6 @@ class Optimizer: | ||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||
|         """ | ||||
|         ones = np.ones(L, dtype=np.int8) | ||||
|  | ||||
|         # Fill-in rows 4 to L+2 | ||||
|         for i in range(1, L-1) :           # Prevent stacked ones | ||||
|             for j in range(L) : | ||||
| @@ -291,7 +287,7 @@ class Optimizer: | ||||
|             if i in vertices_visited : | ||||
|                 h[i*L:i*L+L] = ones | ||||
|  | ||||
|         return h, np.array([len(vertices_visited)-1]) | ||||
|         return h, len(vertices_visited)-1 | ||||
|  | ||||
|  | ||||
|     # Prevents the creation of the same circle (both directions) | ||||
| @@ -320,7 +316,7 @@ class Optimizer: | ||||
|         l[0, g*L + s] = 1 | ||||
|         l[1, s*L + g] = 1 | ||||
|  | ||||
|         return l, np.zeros(2, dtype=np.int8) | ||||
|         return l, [0, 0] | ||||
|  | ||||
|  | ||||
|     def is_connected(self, resx) : | ||||
| @@ -335,13 +331,15 @@ class Optimizer: | ||||
|         """ | ||||
|         resx = np.round(resx).astype(np.int8)  # round all elements and cast them to int | ||||
|          | ||||
|         print(f"resx = ") | ||||
|         # for r in resx : print(r) | ||||
|         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. | ||||
|  | ||||
|         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]  # removed .tolist() | ||||
|         ind_a = nonzero_tup[0] | ||||
|         ind_b = nonzero_tup[1] | ||||
|  | ||||
|         # Extract all journeys | ||||
| @@ -402,7 +400,7 @@ class Optimizer: | ||||
|                         stack.append(neighbor) | ||||
|  | ||||
|         return journey_nodes | ||||
|          | ||||
|  | ||||
|  | ||||
|     def get_order(self, resx): | ||||
|         """ | ||||
| @@ -414,10 +412,8 @@ class Optimizer: | ||||
|         Returns: | ||||
|             list[int]: A list containing the visit order. | ||||
|         """ | ||||
|         resx = np.round(resx).astype(np.uint8)  # must contain only 0 and 1 | ||||
|  | ||||
|         # first round the results to have only 0-1 values | ||||
|         resx = np.round(resx).astype(np.uint8)  # round all elements and cast them to int | ||||
|          | ||||
|         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. | ||||
|  | ||||
| @@ -430,14 +426,14 @@ class Optimizer: | ||||
|         order = [0] | ||||
|         current = 0 | ||||
|         used_indices = set()  # Track visited index pairs | ||||
|          | ||||
|  | ||||
|         while True: | ||||
|             # Find index of the current node in ind_a | ||||
|             try: | ||||
|                 i = ind_a.index(current) | ||||
|             except ValueError: | ||||
|                 break  # No more links, stop the search | ||||
|              | ||||
|  | ||||
|             if i in used_indices: | ||||
|                 break  # Prevent infinite loops | ||||
|  | ||||
| @@ -454,7 +450,6 @@ class Optimizer: | ||||
|         return order | ||||
|  | ||||
|  | ||||
|  | ||||
|     def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] : | ||||
|         """ | ||||
|         Compute the time to reach from each landmark to the next and create a list of landmarks with updated travel times. | ||||
| @@ -466,7 +461,6 @@ class Optimizer: | ||||
|         Returns: | ||||
|             list[Landmark]]: The updated linked list of landmarks with travel times | ||||
|         """ | ||||
|          | ||||
|         L =  [] | ||||
|         j = 0 | ||||
|         while j < len(order)-1 : | ||||
| @@ -485,11 +479,10 @@ class Optimizer: | ||||
|         next.location = (round(next.location[0], 5), round(next.location[1], 5)) | ||||
|         next.must_do = True    | ||||
|         L.append(next) | ||||
|          | ||||
|  | ||||
|         return L | ||||
|  | ||||
|  | ||||
|     # Main optimization pipeline | ||||
|     def solve_optimization( | ||||
|             self, | ||||
|             max_time: int, | ||||
| @@ -527,65 +520,75 @@ class Optimizer: | ||||
|  | ||||
|         self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") | ||||
|  | ||||
|         print(A_eq) | ||||
|         print('\n\n') | ||||
|         print(b_eq) | ||||
|         print('\n\n') | ||||
|  | ||||
|  | ||||
|         # A, b = self.respect_user_must_do(landmarks)                      # Check if there are user_defined must_see. Also takes care of start/goal | ||||
|         # if len(b) > 0 : | ||||
|         #     A_eq = np.vstack((A_eq, A), dtype=np.int8) | ||||
|         #     b_eq += b | ||||
|         # A, b = self.respect_user_must_avoid(landmarks)                      # Check if there are user_defined must_see. Also takes care of start/goal | ||||
|         # if len(b) > 0 : | ||||
|         #     A_eq = np.vstack((A_eq, A), dtype=np.int8) | ||||
|         #     b_eq += b | ||||
|         # A, b = self.respect_start_finish(L)                  # Force start and finish positions | ||||
|         # A_eq = np.vstack((A_eq, A), dtype=np.int8) | ||||
|         # b_eq += b | ||||
|         # A, b = self.respect_order(L)                         # Respect order of visit (only works when max_time is limiting factor) | ||||
|         # A_eq = np.vstack((A_eq, A), dtype=np.int8) | ||||
|         # b_eq += b | ||||
|         # until here opti | ||||
|          | ||||
|         # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) | ||||
|         x_bounds = [(0, 1)]*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) | ||||
|         # Solve linear programming problem with PulP | ||||
|         prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) | ||||
|         x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] | ||||
|          | ||||
|         # Add the objective function | ||||
|         prob += pl.lpSum([c[i] * x[i] for i in range(L*L)]) | ||||
|          | ||||
|         # Add Inequality Constraints (A_ub @ x <= b_ub) | ||||
|         for i in range(len(b_ub)):  # Iterate over rows of A_ub | ||||
|             prob += (pl.lpSum([A_ub[i][j] * x[j] for j in range(L*L)]) <= b_ub[i]) | ||||
|  | ||||
|         # 5. Add Equality Constraints (A_eq @ x == b_eq) | ||||
|         for i in range(len(b_eq)):  # Iterate over rows of A_eq | ||||
|             prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) | ||||
|  | ||||
|         # 6. Solve the problem | ||||
|         prob.solve(pl.PULP_CBC_CMD(msg=True)) | ||||
|  | ||||
|         # 7. Extract Results | ||||
|         status = pl.LpStatus[prob.status] | ||||
|         solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1) | ||||
|  | ||||
|         print(status) | ||||
|  | ||||
|         self.logger.debug("First results are out. Looking out for circles and correcting.") | ||||
|  | ||||
|         # Raise error if no solution is found. FIXME: for now this throws the internal server error | ||||
|         if not res.success : | ||||
|         if status != 'Optimal' : | ||||
|             self.logger.error("The problem is overconstrained, no solution on first try.") | ||||
|             raise ArithmeticError("No solution could be found, the problem is overconstrained. Try with a longer trip (>30 minutes).") | ||||
|             raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.") | ||||
|  | ||||
|         # If there is a solution, we're good to go, just check for connectiveness | ||||
|         circles = self.is_connected(res.x) | ||||
|         #nodes, edges = is_connected(res.x) | ||||
|         circles = self.is_connected(solution) | ||||
|  | ||||
|         i = 0 | ||||
|         timeout = 80 | ||||
|         while circles is not None and i < timeout: | ||||
|             i += 1 | ||||
|             # print(f"Iteration {i} of fixing circles") | ||||
|             A, b = self.prevent_config(res.x) | ||||
|             A_ub = np.vstack((A_ub, A)) | ||||
|             b_ub = np.concatenate((b_ub, b)) | ||||
|  | ||||
|             print("ok1") | ||||
|             A, b = self.prevent_config(solution) | ||||
|             print("ok2") | ||||
|             print(f"A: {A}") | ||||
|             print(f"b: {b}") | ||||
|             try : | ||||
|                 prob += pl.lpSum([A[j] * x[j // L][j % L] for j in range(L * L)]) == b | ||||
|             except Exception as exc : | ||||
|                 self.logger.error(f'Unexpected error occured', details=exc) | ||||
|                 raise Exception from exc | ||||
|      | ||||
|             print("ok3") | ||||
|             for circle in circles : | ||||
|                 A, b = self.prevent_circle(circle, L) | ||||
|                 A_eq = np.vstack((A_eq, A)) | ||||
|                 b_eq = np.concatenate((b_eq, b)) | ||||
|                 prob += (pl.lpSum([A[0][j] * x[j // L][j % L] for j in range(L*L)]) == b[0]) | ||||
|                 prob += (pl.lpSum([A[1][j] * x[j // L][j % L] for j in range(L*L)]) == b[1]) | ||||
|             print("ok4") | ||||
|             prob.solve(pl.PULP_CBC_CMD(msg=True)) | ||||
|  | ||||
|             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) | ||||
|             status = pl.LpStatus[prob.status] | ||||
|             solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1) | ||||
|  | ||||
|             if not res.success : | ||||
|             if status != 'Optimal' : | ||||
|                 self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.') | ||||
|                 raise ArithmeticError("Solving failed because of overconstrained problem") | ||||
|             circles = self.is_connected(res.x) | ||||
|             #nodes, edges = is_connected(res.x) | ||||
|  | ||||
|             circles = self.is_connected(solution) | ||||
|             if circles is None : | ||||
|                 break | ||||
|  | ||||
| @@ -594,8 +597,8 @@ class Optimizer: | ||||
|             raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.") | ||||
|  | ||||
|         # Sort the landmarks in the order of the solution | ||||
|         order = self.get_order(res.x) | ||||
|         order = self.get_order(solution) | ||||
|         tour =  [landmarks[i] for i in order]  | ||||
|  | ||||
|         self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}") | ||||
|         self.logger.debug(f"Re-optimized {i} times, score: {int(pl.value(prob.objective))}") | ||||
|         return tour | ||||
|   | ||||
		Reference in New Issue
	
	Block a user