From f67e2b5dd69acc0ce6f2622a3e7f48fdcd0ce2ae Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Thu, 23 Jan 2025 12:13:51 +0100 Subject: [PATCH] better dosctrings --- backend/src/utils/optimizer.py | 182 +++++++++++++++------------------ backend/src/utils/refiner.py | 1 - 2 files changed, 84 insertions(+), 99 deletions(-) diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 3bbe703..4857858 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -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): """ - Initialize the objective function coefficients and inequality constraints. - -> Adds 1 row of constraints - -> Pre-allocates A_ub for the rest of the computations with L + (L*L-L)/2 rows + Initialize the objective function and inequality constraints for the linear program. - This function computes the distances between all landmarks and stores - their attractiveness to maximize sightseeing. The goal is to maximize - the objective function subject to the constraints A*x < b and A_eq*x = b_eq. + This function sets up the objective to maximize the attractiveness of visiting landmarks, + while ensuring that the total time (including travel and visit duration) does not exceed + 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: - landmarks (list[Landmark]): List of landmarks. - max_time (int): Maximum time of visit allowed. + prob (pl.LpProblem): The linear programming problem where constraints and the objective will be added. + 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: - 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. """ L = len(landmarks) @@ -117,14 +122,20 @@ class Optimizer: 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. - -> Adds L-1 rows of constraints + Generate constraints to ensure each landmark is visited at most once and cap the total number of visited landmarks. + + 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: - 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: - 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 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): """ Generate constraints to prevent simultaneous travel between two landmarks - in both directions. Constraint to not have d14 and d41 simultaneously. - Does not prevent cyclic paths with more elements - -> Adds (L*L-L)/2 rows of constraints (some of which might be zero) + in both directions. This constraint ensures that, for any pair of landmarks, + travel from landmark i to landmark j (dij) and travel from landmark j to landmark i (dji) + 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: - 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: - 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 constraints in-place. """ upper_ind = np.triu_indices(L, 0, L) # Get the upper triangular indices 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): """ - 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 + Generate constraints to prevent staying at the same position during travel. + Specifically, it removes travel from a landmark to itself (e.g., d11, d22, d33, etc.). + + 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: - 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: - 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) @@ -181,18 +201,23 @@ class Optimizer: 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): """ - 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. - -> 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: - 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: - 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. A_eq = np.zeros((3,L*L), dtype=np.int8) @@ -211,27 +236,24 @@ class Optimizer: for i in range(3) : 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): """ Generate constraints to tie the optimization problem together and prevent 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: - 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: - 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 # Loop through rows 1 to L-2 to prevent stacked ones 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]) : """ 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: + 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'. 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) 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) - # 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) : """ - 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: - circle_vertices (list): List of vertices forming a circle. - L (int): Number of landmarks. + prob (pl.LpProblem): The linear programming problem instance to which the constraints will be added. + 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: - 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) @@ -544,12 +541,8 @@ class Optimizer: return prob, x - def solve_optimization( - self, - max_time: int, - landmarks: list[Landmark], - max_landmarks: int = None - ) -> list[Landmark]: + + def solve_optimization(self, max_time: int, landmarks: list[Landmark], max_landmarks: int = None) -> list[Landmark]: """ Main optimization pipeline to solve the landmark visiting problem. @@ -563,14 +556,12 @@ class Optimizer: Returns: 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) 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)) - - # 3. 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) @@ -588,10 +579,6 @@ class Optimizer: timeout = 40 while circles is not None : 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 : self.logger.error(f'Timeout: 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 : break - # Sort the landmarks in the order of the solution order = self.get_order(solution) tour = [landmarks[i] for i in order] diff --git a/backend/src/utils/refiner.py b/backend/src/utils/refiner.py index 013d9fc..a07e9c6 100644 --- a/backend/src/utils/refiner.py +++ b/backend/src/utils/refiner.py @@ -4,7 +4,6 @@ from math import pi import yaml from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull - from ..structs.landmark import Landmark from .get_time_distance import get_time from .take_most_important import take_most_important