better dosctrings
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m26s
Run linting on the backend code / Build (pull_request) Successful in 28s
Run testing on the backend code / Build (pull_request) Failing after 1m27s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 52s

This commit is contained in:
Helldragon67 2025-01-23 12:13:51 +01:00
parent 28ff0460ab
commit f67e2b5dd6
2 changed files with 84 additions and 99 deletions

View File

@ -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]

View File

@ -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