backend/new-overpass #52
| @@ -26,3 +26,4 @@ fastapi-cli = "*" | |||||||
| scikit-learn = "*" | scikit-learn = "*" | ||||||
| pyqt6 = "*" | pyqt6 = "*" | ||||||
| loki-logger-handler = "*" | 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.""" |     """Client used to call the app.""" | ||||||
|     return TestClient(app) |     return TestClient(app) | ||||||
|  |  | ||||||
|  | ''' | ||||||
| def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||||
|     """ |     """ | ||||||
|     Test n°1 : Custom test in Turckheim to ensure small villages are also supported. |     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: |         request: | ||||||
|     """ |     """ | ||||||
|     start_time = time.time()  # Start timer |     start_time = time.time()  # Start timer | ||||||
|     duration_minutes = 15 |     duration_minutes = 20 | ||||||
|  |  | ||||||
|     response = client.post( |     response = client.post( | ||||||
|         "/trip/new", |         "/trip/new", | ||||||
| @@ -45,8 +45,8 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | |||||||
|     # Add details to report |     # Add details to report | ||||||
|     log_trip_details(request, landmarks, result['total_time'], duration_minutes) |     log_trip_details(request, landmarks, result['total_time'], duration_minutes) | ||||||
|  |  | ||||||
|     # for elem in landmarks : |     for elem in landmarks : | ||||||
|     #     print(elem) |         print(elem) | ||||||
|  |  | ||||||
|     # checks : |     # checks : | ||||||
|     assert response.status_code == 200  # check for successful planning |     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 comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" | ||||||
|     assert 2==3 |     assert 2==3 | ||||||
| ''' | ''' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bellecour(client, request) :   # pylint: disable=redefined-outer-name | 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. |     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 |     # Add details to report | ||||||
|     log_trip_details(request, landmarks, result['total_time'], duration_minutes) |     log_trip_details(request, landmarks, result['total_time'], duration_minutes) | ||||||
|  |  | ||||||
|     # for elem in landmarks : |     for elem in landmarks : | ||||||
|     #     print(elem) |         print(elem) | ||||||
|  |  | ||||||
|     # checks : |     # checks : | ||||||
|     assert response.status_code == 200  # check for successful planning |     assert response.status_code == 200  # check for successful planning | ||||||
|     assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" |     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 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 | 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. |     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 yaml, logging | ||||||
| import numpy as np | import numpy as np | ||||||
|  | import pulp as pl | ||||||
| from scipy.optimize import linprog | from scipy.optimize import linprog | ||||||
| from collections import defaultdict, deque | from collections import defaultdict, deque | ||||||
|  |  | ||||||
| @@ -9,6 +9,9 @@ from .get_time_separation import get_time | |||||||
| from ..constants import OPTIMIZER_PARAMETERS_PATH | from ..constants import OPTIMIZER_PARAMETERS_PATH | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logging.getLogger('pulp').setLevel(level=logging.CRITICAL) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Optimizer: | class Optimizer: | ||||||
|  |  | ||||||
|     logger = logging.getLogger(__name__) |     logger = logging.getLogger(__name__) | ||||||
| @@ -62,7 +65,7 @@ class Optimizer: | |||||||
|         b_ub[0] = round(max_time*self.overshoot) |         b_ub[0] = round(max_time*self.overshoot) | ||||||
|  |  | ||||||
|         for i, spot1 in enumerate(landmarks) : |         for i, spot1 in enumerate(landmarks) : | ||||||
|             c[i] = -spot1.attractiveness |             c[i] = spot1.attractiveness | ||||||
|             for j in range(i+1, L) : |             for j in range(i+1, L) : | ||||||
|                 if i !=j : |                 if i !=j : | ||||||
|                     t = get_time(spot1.location, landmarks[j].location) |                     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. |             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 |         # 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 |         # Fill-in row 2 until row L-2 | ||||||
|         for i in range(1, L-1): |         for i in range(1, L-1): | ||||||
|             A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16) |             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  |             tuple[np.ndarray, list[int]]:   Inequality constraint coefficients and  | ||||||
|                                             the right-hand side of the inequality constraints. |                                             the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|         # b = [] |  | ||||||
|         upper_ind = np.triu_indices(L,0,L) |         upper_ind = np.triu_indices(L,0,L) | ||||||
|         up_ind_x = upper_ind[0] |         up_ind_x = upper_ind[0] | ||||||
|         up_ind_y = upper_ind[1] |         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 |         # Fill-in rows L to 2*L-1 | ||||||
|         incr = 0 |         incr = 0 | ||||||
|         for i in range(int((L*L+L)/2)) : |         for i in range(int((L*L+L)/2)) : | ||||||
| @@ -144,8 +143,6 @@ class Optimizer: | |||||||
|                 b[L+incr] = 1 |                 b[L+incr] = 1 | ||||||
|                 incr += 1 |                 incr += 1 | ||||||
|  |  | ||||||
|         # return A[~np.all(A == 0, axis=1)], b |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     def init_eq_not_stay(self, landmarks: list):  |     def init_eq_not_stay(self, landmarks: list):  | ||||||
|         """ |         """ | ||||||
| @@ -220,7 +217,6 @@ class Optimizer: | |||||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|         ones = np.ones(L, dtype=np.int8) |         ones = np.ones(L, dtype=np.int8) | ||||||
|  |  | ||||||
|         # Fill-in rows 4 to L+2 |         # Fill-in rows 4 to L+2 | ||||||
|         for i in range(1, L-1) :           # Prevent stacked ones |         for i in range(1, L-1) :           # Prevent stacked ones | ||||||
|             for j in range(L) : |             for j in range(L) : | ||||||
| @@ -291,7 +287,7 @@ class Optimizer: | |||||||
|             if i in vertices_visited : |             if i in vertices_visited : | ||||||
|                 h[i*L:i*L+L] = ones |                 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) |     # Prevents the creation of the same circle (both directions) | ||||||
| @@ -320,7 +316,7 @@ class Optimizer: | |||||||
|         l[0, g*L + s] = 1 |         l[0, g*L + s] = 1 | ||||||
|         l[1, s*L + g] = 1 |         l[1, s*L + g] = 1 | ||||||
|  |  | ||||||
|         return l, np.zeros(2, dtype=np.int8) |         return l, [0, 0] | ||||||
|  |  | ||||||
|  |  | ||||||
|     def is_connected(self, resx) : |     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 |         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 |         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. |         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] |         nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0] | ||||||
|         nonzero_tup = np.unravel_index(nonzeroind, (L,L)) |         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] |         ind_b = nonzero_tup[1] | ||||||
|  |  | ||||||
|         # Extract all journeys |         # Extract all journeys | ||||||
| @@ -414,9 +412,7 @@ class Optimizer: | |||||||
|         Returns: |         Returns: | ||||||
|             list[int]: A list containing the visit order. |             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 |         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. |         L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def. | ||||||
| @@ -454,7 +450,6 @@ class Optimizer: | |||||||
|         return order |         return order | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] : |     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. |         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: |         Returns: | ||||||
|             list[Landmark]]: The updated linked list of landmarks with travel times |             list[Landmark]]: The updated linked list of landmarks with travel times | ||||||
|         """ |         """ | ||||||
|          |  | ||||||
|         L =  [] |         L =  [] | ||||||
|         j = 0 |         j = 0 | ||||||
|         while j < len(order)-1 : |         while j < len(order)-1 : | ||||||
| @@ -489,7 +483,6 @@ class Optimizer: | |||||||
|         return L |         return L | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Main optimization pipeline |  | ||||||
|     def solve_optimization( |     def solve_optimization( | ||||||
|             self, |             self, | ||||||
|             max_time: int, |             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.") |         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) |         # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) | ||||||
|         x_bounds = [(0, 1)]*L*L |         x_bounds = [(0, 1)]*L*L | ||||||
|  |  | ||||||
|         # Solve linear programming problem |         # Solve linear programming problem with PulP | ||||||
|         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) |         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.") |         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 |         # 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.") |             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 |         # If there is a solution, we're good to go, just check for connectiveness | ||||||
|         circles = self.is_connected(res.x) |         circles = self.is_connected(solution) | ||||||
|         #nodes, edges = is_connected(res.x) |  | ||||||
|         i = 0 |         i = 0 | ||||||
|         timeout = 80 |         timeout = 80 | ||||||
|         while circles is not None and i < timeout: |         while circles is not None and i < timeout: | ||||||
|             i += 1 |             i += 1 | ||||||
|             # print(f"Iteration {i} of fixing circles") |             # print(f"Iteration {i} of fixing circles") | ||||||
|             A, b = self.prevent_config(res.x) |             print("ok1") | ||||||
|             A_ub = np.vstack((A_ub, A)) |             A, b = self.prevent_config(solution) | ||||||
|             b_ub = np.concatenate((b_ub, b)) |             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 : |             for circle in circles : | ||||||
|                 A, b = self.prevent_circle(circle, L) |                 A, b = self.prevent_circle(circle, L) | ||||||
|                 A_eq = np.vstack((A_eq, A)) |                 prob += (pl.lpSum([A[0][j] * x[j // L][j % L] for j in range(L*L)]) == b[0]) | ||||||
|                 b_eq = np.concatenate((b_eq, b)) |                 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.') |                 self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.') | ||||||
|                 raise ArithmeticError("Solving failed because of overconstrained problem") |                 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 : |             if circles is None : | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
| @@ -594,8 +597,8 @@ class Optimizer: | |||||||
|             raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.") |             raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.") | ||||||
|  |  | ||||||
|         # Sort the landmarks in the order of the solution |         # 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]  |         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 |         return tour | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user