backend/new-overpass #52
| @@ -59,20 +59,25 @@ class Optimizer: | |||||||
|  |  | ||||||
|     def init_ub_time(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark], max_time: int): |     def init_ub_time(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark], max_time: int): | ||||||
|         """ |         """ | ||||||
|         Initialize the objective function coefficients and inequality constraints. |         Initialize the objective function and inequality constraints for the linear program. | ||||||
|         -> Adds 1 row of constraints |  | ||||||
|         -> Pre-allocates A_ub for the rest of the computations with L + (L*L-L)/2 rows |  | ||||||
|  |  | ||||||
|         This function computes the distances between all landmarks and stores |         This function sets up the objective to maximize the attractiveness of visiting landmarks, | ||||||
|         their attractiveness to maximize sightseeing. The goal is to maximize  |         while ensuring that the total time (including travel and visit duration) does not exceed | ||||||
|         the objective function subject to the constraints A*x < b and A_eq*x = b_eq. |         the maximum allowed time. It calculates the pairwise travel times between landmarks and | ||||||
|  |         incorporates visit duration to form the inequality constraints. | ||||||
|  |  | ||||||
|  |         The objective is to maximize sightseeing by selecting the most attractive landmarks within | ||||||
|  |         the time limit. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             landmarks (list[Landmark]): List of landmarks. |             prob (pl.LpProblem): The linear programming problem where constraints and the objective will be added. | ||||||
|             max_time (int): Maximum time of visit allowed. |             x (pl.LpVariable): A decision variable representing whether a landmark is visited. | ||||||
|  |             L (int): The number of landmarks. | ||||||
|  |             landmarks (list[Landmark]): List of landmarks to visit. | ||||||
|  |             max_time (int): Maximum allowable time for sightseeing, including travel and visit duration. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality |             None: Adds the objective function and constraints to the LP problem directly. | ||||||
|             constraint coefficients, and the right-hand side of the inequality constraint. |             constraint coefficients, and the right-hand side of the inequality constraint. | ||||||
|         """ |         """ | ||||||
|         L = len(landmarks) |         L = len(landmarks) | ||||||
| @@ -117,14 +122,20 @@ class Optimizer: | |||||||
|  |  | ||||||
|     def respect_number(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, max_landmarks: int): |     def respect_number(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, max_landmarks: int): | ||||||
|         """ |         """ | ||||||
|         Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks. |         Generate constraints to ensure each landmark is visited at most once and cap the total number of visited landmarks. | ||||||
|         -> Adds L-1 rows of constraints |  | ||||||
|  |         This function adds the following constraints to the linear program: | ||||||
|  |         1. Each landmark is visited at most once by creating L-2 constraints (one for each landmark). | ||||||
|  |         2. The total number of visited landmarks is capped by the specified maximum number (`max_landmarks`) plus 2. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             L (int): Number of landmarks. |             prob (pl.LpProblem): The linear programming problem where constraints will be added. | ||||||
|  |             x (pl.LpVariable): Decision variable indicating whether a landmark is visited. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|  |             max_landmarks (int): The maximum number of landmarks that can be visited. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             None: This function directly modifies the `prob` object by adding constraints. | ||||||
|         """ |         """ | ||||||
|         # L-2 constraints: each landmark is visited exactly once |         # L-2 constraints: each landmark is visited exactly once | ||||||
|         for i in range(1, L-1): |         for i in range(1, L-1): | ||||||
| @@ -137,16 +148,20 @@ class Optimizer: | |||||||
|     def break_sym(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): |     def break_sym(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): | ||||||
|         """ |         """ | ||||||
|         Generate constraints to prevent simultaneous travel between two landmarks |         Generate constraints to prevent simultaneous travel between two landmarks | ||||||
|         in both directions. Constraint to not have d14 and d41 simultaneously.  |         in both directions. This constraint ensures that, for any pair of landmarks, | ||||||
|         Does not prevent cyclic paths with more elements |         travel from landmark i to landmark j (dij) and travel from landmark j to landmark i (dji) | ||||||
|         -> Adds (L*L-L)/2 rows of constraints (some of which might be zero) |         cannot happen simultaneously. | ||||||
|  |  | ||||||
|  |         This method adds constraints to break symmetry, specifically to prevent | ||||||
|  |         cyclic paths with only two elements. It does not prevent cyclic paths involving more than two elements. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             L (int): Number of landmarks. |             prob (pl.LpProblem): The linear programming problem where constraints will be added. | ||||||
|  |             x (pl.LpVariable): Decision variable representing travel between landmarks. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[np.ndarray, list[int]]:   Inequality constraint coefficients and  |             None: This function modifies the `prob` object by adding constraints in-place. | ||||||
|                                             the right-hand side of the inequality constraints. |  | ||||||
|         """ |         """ | ||||||
|         upper_ind = np.triu_indices(L, 0, L)  # Get the upper triangular indices |         upper_ind = np.triu_indices(L, 0, L)  # Get the upper triangular indices | ||||||
|         up_ind_x = upper_ind[0] |         up_ind_x = upper_ind[0] | ||||||
| @@ -161,15 +176,20 @@ class Optimizer: | |||||||
|  |  | ||||||
|     def init_eq_not_stay(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): |     def init_eq_not_stay(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): | ||||||
|         """ |         """ | ||||||
|         Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.). |         Generate constraints to prevent staying at the same position during travel.  | ||||||
|         -> Adds 1 row of constraints |         Specifically, it removes travel from a landmark to itself (e.g., d11, d22, d33, etc.). | ||||||
|         -> Pre-allocates A_eq for the rest of the computations with (L+ 2 + dynamic incr) rows |  | ||||||
|  |         This function adds one equality constraint to the optimization problem that ensures  | ||||||
|  |         no decision variable corresponding to staying at the same landmark is included  | ||||||
|  |         in the solution. This helps in ensuring that the path does not include self-loops. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             L (int): Number of landmarks. |             prob (pl.LpProblem): The linear programming problem where constraints will be added. | ||||||
|  |             x (pl.LpVariable): Decision variable representing travel between landmarks. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints. |             None: This function modifies the `prob` object by adding an equality constraint in-place. | ||||||
|         """ |         """ | ||||||
|         A_eq = np.zeros((L, L), dtype=np.int8) |         A_eq = np.zeros((L, L), dtype=np.int8) | ||||||
|  |  | ||||||
| @@ -181,18 +201,23 @@ class Optimizer: | |||||||
|         prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 0) |         prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 0) | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Constraint to ensure start at start and finish at goal |  | ||||||
|     def respect_start_finish(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): |     def respect_start_finish(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): | ||||||
|         """ |         """ | ||||||
|         Generate constraints to ensure that the optimization starts at the designated  |         Generate constraints to ensure that the optimization starts at the designated  | ||||||
|         start landmark and finishes at the goal landmark. |         start landmark and finishes at the goal landmark. | ||||||
|         -> Adds 3 rows of constraints |  | ||||||
|  |         Specifically, this function adds three equality constraints: | ||||||
|  |         1. Ensures that the path starts at the designated start landmark (row 0). | ||||||
|  |         2. Ensures that the path finishes at the designated goal landmark (row 1). | ||||||
|  |         3. Prevents any arrivals at the start landmark or departures from the goal landmark (row 2). | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             L (int): Number of landmarks. |             prob (pl.LpProblem): The linear programming problem where constraints will be added. | ||||||
|  |             x (pl.LpVariable): Decision variable representing travel between landmarks. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             None: This function modifies the `prob` object by adding three equality constraints in-place. | ||||||
|         """ |         """ | ||||||
|         # Fill-in row 0. |         # Fill-in row 0. | ||||||
|         A_eq = np.zeros((3,L*L), dtype=np.int8) |         A_eq = np.zeros((3,L*L), dtype=np.int8) | ||||||
| @@ -211,27 +236,24 @@ class Optimizer: | |||||||
|         for i in range(3) : |         for i in range(3) : | ||||||
|             prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) |             prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) | ||||||
|  |  | ||||||
|  |  | ||||||
|     def respect_order(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): |     def respect_order(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): | ||||||
|         """ |         """ | ||||||
|         Generate constraints to tie the optimization problem together and prevent  |         Generate constraints to tie the optimization problem together and prevent  | ||||||
|         stacked ones, although this does not fully prevent circles. |         stacked ones, although this does not fully prevent circles. | ||||||
|         -> Adds L-2 rows of constraints |  | ||||||
|  |         This function adds constraints to the optimization problem that prevent  | ||||||
|  |         simultaneous travel between landmarks in a way that would result in stacked ones. | ||||||
|  |         However, it does not fully prevent circular paths. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             L (int): Number of landmarks. |             prob (pl.LpProblem): The linear programming problem where constraints will be added. | ||||||
|  |             x (pl.LpVariable): Decision variable representing travel between landmarks. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             None: This function modifies the `prob` object by adding L-2 equality constraints in-place. | ||||||
|         """ |         """ | ||||||
|         # A_eq = np.zeros(L*L, dtype=np.int8) |  | ||||||
|         # 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) : |  | ||||||
|         #         A_eq[i + j*L] = -1 |  | ||||||
|         #     A_eq[i*L:(i+1)*L] = ones |  | ||||||
|         #     prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 0) |  | ||||||
|  |  | ||||||
|         # FIXME: weird 0 artifact in the coefficients popping up |         # FIXME: weird 0 artifact in the coefficients popping up | ||||||
|         # Loop through rows 1 to L-2 to prevent stacked ones |         # Loop through rows 1 to L-2 to prevent stacked ones | ||||||
|         for i in range(1, L-1): |         for i in range(1, L-1): | ||||||
| @@ -243,14 +265,19 @@ class Optimizer: | |||||||
|     def respect_user_must(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark]) : |     def respect_user_must(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark]) : | ||||||
|         """ |         """ | ||||||
|         Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization. |         Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization. | ||||||
|         -> Adds a variable number of rows of constraints BUT CAN BE PRE COMPUTED |  | ||||||
|  |  | ||||||
|  |         This function adds constraints to the optimization problem to ensure that landmarks marked as  | ||||||
|  |         'must_do' are included in the solution. It precomputes the constraints and adds them to the  | ||||||
|  |         problem accordingly. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|  |             prob (pl.LpProblem): The linear programming problem where constraints will be added. | ||||||
|  |             x (pl.LpVariable): Decision variable representing travel between landmarks. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|             landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'. |             landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             None: This function modifies the `prob` object by adding equality constraints in-place. | ||||||
|         """ |         """ | ||||||
|         ones = np.ones(L, dtype=np.int8) |         ones = np.ones(L, dtype=np.int8) | ||||||
|         A_eq = np.zeros(L*L, dtype=np.int8) |         A_eq = np.zeros(L*L, dtype=np.int8) | ||||||
| @@ -264,52 +291,22 @@ class Optimizer: | |||||||
|                 prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 2) |                 prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 2) | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Prevent the use of a particular solution. TODO probably can be done faster just using resx |  | ||||||
|     # def prevent_config(self, prob: pl.LpProblem, x: pl.LpVariable, resx): |  | ||||||
|     #     """ |  | ||||||
|     #     Prevent the use of a particular solution by adding constraints to the optimization. |  | ||||||
|  |  | ||||||
|     #     Args: |  | ||||||
|     #         resx (list[float]): List of edge weights. |  | ||||||
|  |  | ||||||
|     #     Returns: |  | ||||||
|     #         tuple[list[int], list[int]]: A tuple containing a new row for A and new value for ub. |  | ||||||
|     #     """ |  | ||||||
|  |  | ||||||
|     #     for i, elem in enumerate(resx): |  | ||||||
|     #         resx[i] = round(elem) |  | ||||||
|  |  | ||||||
|     #     N = len(resx)               # Number of edges |  | ||||||
|     #     L = int(np.sqrt(N))         # Number of landmarks |  | ||||||
|  |  | ||||||
|     #     nonzeroind = np.nonzero(resx)[0]                    # the return is a little funky so I use the [0] |  | ||||||
|     #     nonzero_tup = np.unravel_index(nonzeroind, (L,L)) |  | ||||||
|  |  | ||||||
|     #     ind_a = nonzero_tup[0].tolist() |  | ||||||
|     #     vertices_visited = ind_a |  | ||||||
|     #     vertices_visited.remove(0) |  | ||||||
|  |  | ||||||
|     #     ones = np.ones(L, dtype=np.int8) |  | ||||||
|     #     h = np.zeros(L*L, dtype=np.int8) |  | ||||||
|  |  | ||||||
|     #     for i in range(L) : |  | ||||||
|     #         if i in vertices_visited : |  | ||||||
|     #             h[i*L:i*L+L] = ones |  | ||||||
|  |  | ||||||
|     #     return h, len(vertices_visited)-1 |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Prevents the creation of the same circle (both directions) |  | ||||||
|     def prevent_circle(self, prob: pl.LpProblem, x: pl.LpVariable, circle_vertices: list, L: int) : |     def prevent_circle(self, prob: pl.LpProblem, x: pl.LpVariable, circle_vertices: list, L: int) : | ||||||
|         """ |         """ | ||||||
|         Prevent circular paths by by adding constraints to the optimization. |         Prevent circular paths by adding constraints to the optimization. | ||||||
|  |  | ||||||
|  |         This function ensures that circular paths in both directions (i.e., forward and reverse)  | ||||||
|  |         between landmarks are avoided in the optimization problem by adding the corresponding constraints. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             circle_vertices (list): List of vertices forming a circle. |             prob (pl.LpProblem): The linear programming problem instance to which the constraints will be added. | ||||||
|             L (int): Number of landmarks. |             x (pl.LpVariable): Decision variable representing the travel between landmarks in the problem. | ||||||
|  |             circle_vertices (list): List of indices representing the landmarks that form a circular path. | ||||||
|  |             L (int): The total number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector. |             None: This function modifies the `prob` object by adding two equality constraints that  | ||||||
|  |                 prevent circular paths in both directions for the specified circle vertices. | ||||||
|         """ |         """ | ||||||
|         l = np.zeros((2, L*L), dtype=np.int8) |         l = np.zeros((2, L*L), dtype=np.int8) | ||||||
|  |  | ||||||
| @@ -544,12 +541,8 @@ class Optimizer: | |||||||
|  |  | ||||||
|         return prob, x |         return prob, x | ||||||
|  |  | ||||||
|     def solve_optimization( |  | ||||||
|             self, |     def solve_optimization(self, max_time: int, landmarks: list[Landmark], max_landmarks: int = None) -> list[Landmark]: | ||||||
|             max_time: int, |  | ||||||
|             landmarks: list[Landmark], |  | ||||||
|             max_landmarks: int = None |  | ||||||
|         ) -> list[Landmark]: |  | ||||||
|         """ |         """ | ||||||
|         Main optimization pipeline to solve the landmark visiting problem. |         Main optimization pipeline to solve the landmark visiting problem. | ||||||
|  |  | ||||||
| @@ -563,14 +556,12 @@ class Optimizer: | |||||||
|         Returns: |         Returns: | ||||||
|             list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found. |             list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found. | ||||||
|         """ |         """ | ||||||
|         # 1. Setup the optimization proplem. |         # Setup the optimization proplem. | ||||||
|         L = len(landmarks) |         L = len(landmarks) | ||||||
|         prob, x = self.pre_processing(L, landmarks, max_time, max_landmarks) |         prob, x = self.pre_processing(L, landmarks, max_time, max_landmarks) | ||||||
|  |  | ||||||
|         # 2. Solve the problem |         # Solve the problem and extract results. | ||||||
|         prob.solve(pl.PULP_CBC_CMD(msg=False, gapRel=0.1)) |         prob.solve(pl.PULP_CBC_CMD(msg=False, gapRel=0.1)) | ||||||
|  |  | ||||||
|         # 3. Extract Results |  | ||||||
|         status = pl.LpStatus[prob.status] |         status = pl.LpStatus[prob.status] | ||||||
|         solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1) |         solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1) | ||||||
|  |  | ||||||
| @@ -588,10 +579,6 @@ class Optimizer: | |||||||
|         timeout = 40 |         timeout = 40 | ||||||
|         while circles is not None : |         while circles is not None : | ||||||
|             i += 1 |             i += 1 | ||||||
|             # print(f"Iteration {i} of fixing circles") |  | ||||||
|             # l, b = self.prevent_config(solution) |  | ||||||
|             # prob += (pl.lpSum([l[j] * x[j] for j in range(L*L)]) == b) |  | ||||||
|  |  | ||||||
|             if i == timeout : |             if i == timeout : | ||||||
|                 self.logger.error(f'Timeout: No solution found after {timeout} iterations.') |                 self.logger.error(f'Timeout: No solution found after {timeout} iterations.') | ||||||
|                 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.") | ||||||
| @@ -611,7 +598,6 @@ class Optimizer: | |||||||
|             if circles is None : |             if circles is None : | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|  |  | ||||||
|         # Sort the landmarks in the order of the solution |         # Sort the landmarks in the order of the solution | ||||||
|         order = self.get_order(solution) |         order = self.get_order(solution) | ||||||
|         tour =  [landmarks[i] for i in order] |         tour =  [landmarks[i] for i in order] | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ from math import pi | |||||||
| import yaml | import yaml | ||||||
| from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull | from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull | ||||||
|  |  | ||||||
|  |  | ||||||
| from ..structs.landmark import Landmark | from ..structs.landmark import Landmark | ||||||
| from .get_time_distance import get_time | from .get_time_distance import get_time | ||||||
| from .take_most_important import take_most_important | from .take_most_important import take_most_important | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user