some rpoblem
This commit is contained in:
		| @@ -21,7 +21,7 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|         request: | ||||
|     """ | ||||
|     start_time = time.time()  # Start timer | ||||
|     duration_minutes = 20 | ||||
|     duration_minutes = 15 | ||||
|  | ||||
|     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 | ||||
| @@ -54,9 +54,9 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name | ||||
|     assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 | ||||
|     assert len(landmarks) > 2           # check that there is something to visit | ||||
|     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 | ||||
|     """ | ||||
| @@ -219,7 +219,7 @@ def test_shopping(client, request) :   # pylint: disable=redefined-outer-name | ||||
|     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 | ||||
|  | ||||
|  | ||||
| ''' | ||||
| # def test_new_trip_single_prefs(client): | ||||
| #     response = client.post( | ||||
| #         "/trip/new", | ||||
|   | ||||
| @@ -21,7 +21,8 @@ class Optimizer: | ||||
|     average_walking_speed: float    # average walking speed of adult | ||||
|     max_landmarks: int              # max number of landmarks to visit | ||||
|     overshoot: float                # overshoot to allow maxtime to overflow. Optimizer is a bit restrictive | ||||
|  | ||||
|     prob: pl.LpProblem              # linear optimization problem to solve | ||||
|     x: list[pl.LpVariable]          # decision variables | ||||
|  | ||||
|     def __init__(self) : | ||||
|  | ||||
| @@ -32,9 +33,12 @@ class Optimizer: | ||||
|             self.average_walking_speed = parameters['average_walking_speed'] | ||||
|             self.max_landmarks = parameters['max_landmarks'] | ||||
|             self.overshoot = parameters['overshoot'] | ||||
|          | ||||
|         # Initalize the optimization problem | ||||
|         self.prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) | ||||
|  | ||||
|  | ||||
|     def init_ub_time(self, landmarks: list[Landmark], max_time: int): | ||||
|     def init_ub_time(self, L: int, landmarks: list[Landmark], max_time: int): | ||||
|         """ | ||||
|         Initialize the objective function coefficients and inequality constraints. | ||||
|         -> Adds 1 row of constraints | ||||
| @@ -57,23 +61,20 @@ class Optimizer: | ||||
|         # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... | ||||
|         c = np.zeros(L, dtype=np.int16) | ||||
|  | ||||
|         # Coefficients of inequality constraints (left-hand side) | ||||
|         A_ub = np.zeros((L + int((L*L-L)/2), L*L), dtype=np.int16) | ||||
|         b_ub = np.zeros(L + int((L*L-L)/2), dtype=np.int16) | ||||
|  | ||||
|         # Fill in first row | ||||
|         b_ub[0] = round(max_time*self.overshoot) | ||||
|         # inequality matrix and vector | ||||
|         A_ub = np.zeros(L*L, dtype=np.int16) | ||||
|         b_ub = round(max_time*self.overshoot) | ||||
|  | ||||
|         for i, spot1 in enumerate(landmarks) : | ||||
|             c[i] = spot1.attractiveness | ||||
|             for j in range(i+1, L) : | ||||
|                 if i !=j : | ||||
|                     t = get_time(spot1.location, landmarks[j].location) | ||||
|                     A_ub[0, i*L + j] = t + spot1.duration | ||||
|                     A_ub[0, j*L + i] = t + landmarks[j].duration | ||||
|                     A_ub[i*L + j] = t + spot1.duration | ||||
|                     A_ub[j*L + i] = t + landmarks[j].duration | ||||
|  | ||||
|         # Expand 'c' to L*L for every decision variable | ||||
|         c = np.tile(c, L)      | ||||
|         # Expand 'c' to L*L for every decision variable and ad | ||||
|         c = np.tile(c, L) | ||||
|  | ||||
|         # Now sort and modify A_ub for each row | ||||
|         if L > 22 : | ||||
| @@ -90,10 +91,12 @@ class Optimizer: | ||||
|                 row_values[mask] = 32765 | ||||
|                 A_ub[0, i*L:i*L+L] = row_values | ||||
|  | ||||
|         return c, A_ub, b_ub | ||||
|         # Add the objective and the distance constraint | ||||
|         self.prob += pl.lpSum([c[j] * self.x[j] for j in range(L*L)]) | ||||
|         self.prob += (pl.lpSum([A_ub[j] * self.x[j] for j in range(L*L)]) <= b_ub) | ||||
|  | ||||
|  | ||||
|     def respect_number(self, A, b, L, max_landmarks: int): | ||||
|     def respect_number(self, 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 | ||||
| @@ -105,18 +108,16 @@ 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 | ||||
|         # 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) | ||||
|             b[i] = 1 | ||||
|         A_ub = np.zeros(L*L, dtype=np.int8) | ||||
|         for i in range(0, L-2): | ||||
|             A_ub[L*i:L*(i+1)] = np.ones(L, dtype=np.int16) | ||||
|             self.prob += (pl.lpSum([A_ub[j] * self.x[j] for j in range(L*L)]) <= 1) | ||||
|  | ||||
|         # Fill-in row L-1 | ||||
|         # Second constraint: cap the total number of visits | ||||
|         A[L-1, :] = np.ones(L*L, dtype=np.int16) | ||||
|         b[L-1] = max_landmarks+2 | ||||
|         self.prob += (pl.lpSum([1 * self.x[j] for j in range(L*L)]) <= max_landmarks+2) | ||||
|  | ||||
|  | ||||
|     def break_sym(self, A, b, L): | ||||
|     def break_sym(self, L: int): | ||||
|         """ | ||||
|         Generate constraints to prevent simultaneous travel between two landmarks | ||||
|         in both directions. Constraint to not have d14 and d41 simultaneously.  | ||||
| @@ -133,18 +134,17 @@ class Optimizer: | ||||
|         upper_ind = np.triu_indices(L,0,L) | ||||
|         up_ind_x = upper_ind[0] | ||||
|         up_ind_y = upper_ind[1] | ||||
|         A = np.zeros(L*L, dtype=np.int8) | ||||
|  | ||||
|         # Fill-in rows L to 2*L-1 | ||||
|         incr = 0 | ||||
|         for i in range(int((L*L+L)/2)) : | ||||
|             if up_ind_x[i] != up_ind_y[i] : | ||||
|                 A[L+incr, up_ind_x[i]*L + up_ind_y[i]] = 1 | ||||
|                 A[L+incr, up_ind_y[i]*L + up_ind_x[i]] = 1 | ||||
|                 b[L+incr] = 1 | ||||
|                 incr += 1 | ||||
|                 A[up_ind_x[i]*L + up_ind_y[i]] = 1 | ||||
|                 A[up_ind_y[i]*L + up_ind_x[i]] = 1 | ||||
|             self.prob += (pl.lpSum([A[j] * self.x[j] for j in range(L*L)]) <= 1) | ||||
|  | ||||
|  | ||||
|     def init_eq_not_stay(self, landmarks: list):  | ||||
|     def init_eq_not_stay(self, L: int):  | ||||
|         """ | ||||
|         Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.). | ||||
|         -> Adds 1 row of constraints | ||||
| @@ -156,28 +156,17 @@ class Optimizer: | ||||
|         Returns: | ||||
|             tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints. | ||||
|         """ | ||||
|         L = len(landmarks) | ||||
|         incr = 0 | ||||
|         for i, elem in enumerate(landmarks) : | ||||
|             if (elem.must_do or elem.must_avoid) and i not in [0, L-1]: | ||||
|                 incr += 1 | ||||
|  | ||||
|         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) | ||||
|         A_eq = np.zeros((L, L), dtype=np.int8) | ||||
|  | ||||
|         # Set diagonal elements to 1 (to prevent staying in the same position) | ||||
|         np.fill_diagonal(l, 1) | ||||
|         np.fill_diagonal(A_eq, 1) | ||||
|         A_eq = A_eq.flatten() | ||||
|  | ||||
|         # Fill-in first row | ||||
|         A_eq[0,:] = l.flatten() | ||||
|         b_eq[0] = 0 | ||||
|  | ||||
|         return A_eq, b_eq | ||||
|         self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 1) | ||||
|  | ||||
|  | ||||
|     # Constraint to ensure start at start and finish at goal | ||||
|     def respect_start_finish(self, A, b, L: int): | ||||
|     def respect_start_finish(self, L: int): | ||||
|         """ | ||||
|         Generate constraints to ensure that the optimization starts at the designated | ||||
|         start landmark and finishes at the goal landmark. | ||||
| @@ -189,22 +178,24 @@ class Optimizer: | ||||
|         Returns: | ||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||
|         """ | ||||
|         # Fill-in row 1. | ||||
|         A[1, :L] = np.ones(L, dtype=np.int8)        # sets departures only for start (horizontal ones) | ||||
|         # Fill-in row 0. | ||||
|         A_eq = np.zeros((3,L*L), dtype=np.int8) | ||||
|         A_eq[0, :L] = np.ones(L, dtype=np.int8)        # sets departures only for start (horizontal ones) | ||||
|         for k in range(L-1) : | ||||
|             if k != 0 : | ||||
|                 # Fill-in row 2 | ||||
|                 A[2, k*L+L-1] = 1                   # sets arrivals only for finish (vertical ones) | ||||
|             # Fill-in row 3 | ||||
|             A[3, k*L] = 1 | ||||
|                 # Fill-in row 1 | ||||
|                 A_eq[1, k*L+L-1] = 1                   # sets arrivals only for finish (vertical ones) | ||||
|             # Fill-in row 1 | ||||
|             A_eq[2, k*L] = 1 | ||||
|  | ||||
|         A[3, L*(L-1):] = np.ones(L, dtype=np.int8)  # prevents arrivals at start and departures from goal | ||||
|         b[1:4] = [1, 1, 0] | ||||
|         A_eq[2, L*(L-1):] = np.ones(L, dtype=np.int8)  # prevents arrivals at start and departures from goal | ||||
|         b_eq= [1, 1, 0] | ||||
|  | ||||
|         # return A, b | ||||
|         # Add the constraints to pulp | ||||
|         for i in range(3) : | ||||
|             self.prob += (pl.lpSum([A_eq[i][j] * self.x[j] for j in range(L*L)]) == b_eq[i]) | ||||
|  | ||||
|  | ||||
|     def respect_order(self, A, b, L: int):  | ||||
|     def respect_order(self, L: int):  | ||||
|         """ | ||||
|         Generate constraints to tie the optimization problem together and prevent  | ||||
|         stacked ones, although this does not fully prevent circles. | ||||
| @@ -216,17 +207,17 @@ class Optimizer: | ||||
|         Returns: | ||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||
|         """ | ||||
|         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[i-1+4, i + j*L] = -1 | ||||
|             A[i-1+4, i*L:(i+1)*L] = ones | ||||
|  | ||||
|         b[4:L+2] = np.zeros(L-2, dtype=np.int8) | ||||
|                 A_eq[i + j*L] = -1 | ||||
|             A_eq[i*L:(i+1)*L] = ones | ||||
|             self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 0) | ||||
|  | ||||
|  | ||||
|     def respect_user_must(self, A, b, landmarks: list[Landmark]) : | ||||
|     def respect_user_must(self, 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 | ||||
| @@ -238,21 +229,16 @@ class Optimizer: | ||||
|         Returns: | ||||
|             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||
|         """ | ||||
|         L = len(landmarks) | ||||
|         ones = np.ones(L, dtype=np.int8) | ||||
|         incr = 0 | ||||
|         A_eq = np.zeros(L*L, dtype=np.int8) | ||||
|  | ||||
|         for i, elem in enumerate(landmarks) : | ||||
|             if elem.must_do is True and i not in [0, L-1]: | ||||
|                 # First part of the dynamic infill | ||||
|                 A[L+2+incr, i*L:i*L+L] = ones | ||||
|                 b[L+2+incr] = 1 | ||||
|                 incr += 1 | ||||
|                 A_eq[i*L:i*L+L] = ones | ||||
|                 self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 1) | ||||
|             if elem.must_avoid is True and i not in [0, L-1]: | ||||
|                 # Second part of the dynamic infill | ||||
|                 A[L+2+incr, i*L:i*L+L] = ones | ||||
|                 b[L+2+incr] = 0 | ||||
|                 incr += 1 | ||||
|                 A_eq[i*L:i*L+L] = ones | ||||
|                 self.prob += (pl.lpSum([A_eq[j] * self.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 | ||||
| @@ -316,9 +302,11 @@ class Optimizer: | ||||
|         l[0, g*L + s] = 1 | ||||
|         l[1, s*L + g] = 1 | ||||
|  | ||||
|         return l, [0, 0] | ||||
|  | ||||
|         # Add the constraints | ||||
|         self.prob += (pl.lpSum([l[0][j] * self.x[j] for j in range(L*L)]) == 0) | ||||
|         self.prob += (pl.lpSum([l[1][j] * self.x[j] for j in range(L*L)]) == 0) | ||||
|  | ||||
|              | ||||
|     def is_connected(self, resx) : | ||||
|         """ | ||||
|         Determine the order of visits and detect any circular paths in the given configuration. | ||||
| @@ -481,6 +469,27 @@ class Optimizer: | ||||
|         return L | ||||
|  | ||||
|  | ||||
|     def pre_processing(self, L: int, landmarks: list[Landmark], max_time: int, max_landmarks: int | None) : | ||||
|  | ||||
|         if max_landmarks is None : | ||||
|             max_landmarks = self.max_landmarks | ||||
|  | ||||
|         # Define the problem | ||||
|         x_bounds = [(0, 1)]*L*L | ||||
|         self.x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] | ||||
|  | ||||
|         # Setup the inequality constraints | ||||
|         self.init_ub_time(L, landmarks, max_time)   # Adds the distances from each landmark to the other. | ||||
|         self.respect_number(L, max_landmarks)                      # Respects max number of visits (no more possible stops than landmarks). | ||||
|         self.break_sym(L)                           # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. | ||||
|  | ||||
|         # Setup the equality constraints | ||||
|         self.init_eq_not_stay(L)                # Force solution not to stay in same place | ||||
|         self.respect_start_finish(L)            # Force start and finish positions | ||||
|         self.respect_order(L)                   # Respect order of visit (only works when max_time is limiting factor) | ||||
|         self.respect_user_must(L, landmarks)    # Force to do/avoid landmarks set by user. | ||||
|  | ||||
|  | ||||
|     def solve_optimization( | ||||
|             self, | ||||
|             max_time: int, | ||||
| @@ -500,48 +509,16 @@ class Optimizer: | ||||
|         Returns: | ||||
|             list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found. | ||||
|         """ | ||||
|         if max_landmarks is None : | ||||
|             max_landmarks = self.max_landmarks | ||||
|  | ||||
|         # 1. Setup the optimization proplem. | ||||
|         L = len(landmarks) | ||||
|         self.pre_processing(L, landmarks, max_time, max_landmarks) | ||||
|  | ||||
|         # SET CONSTRAINTS FOR INEQUALITY | ||||
|         c, A_ub, b_ub = self.init_ub_time(landmarks, max_time)  # Adds the distances from each landmark to the other. | ||||
|         self.respect_number(A_ub, b_ub, L, max_landmarks)       # Respects max number of visits (no more possible stops than landmarks). | ||||
|         self.break_sym(A_ub, b_ub, L)                           # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. | ||||
|         # 2. Solve the problem | ||||
|         self.prob.solve(pl.PULP_CBC_CMD(msg=True, gapRel=0.1)) | ||||
|  | ||||
|         # SET CONSTRAINTS FOR EQUALITY | ||||
|         A_eq, b_eq = self.init_eq_not_stay(landmarks)           # Force solution not to stay in same place | ||||
|         self.respect_start_finish(A_eq, b_eq, L)                # Force start and finish positions | ||||
|         self.respect_order(A_eq, b_eq, L)                       # Respect order of visit (only works when max_time is limiting factor) | ||||
|         self.respect_user_must(A_eq, b_eq, landmarks)           # Force to do/avoid landmarks set by user. | ||||
|  | ||||
|         self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") | ||||
|  | ||||
|         # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) | ||||
|         x_bounds = [(0, 1)]*L*L | ||||
|  | ||||
|         # 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=False, gapRel=0.3)) | ||||
|  | ||||
|         # 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) | ||||
|         # 3. Extract Results | ||||
|         status = pl.LpStatus[self.prob.status] | ||||
|         solution = [pl.value(var) for var in self.x]  # The values of the decision variables (will be 0 or 1) | ||||
|  | ||||
|         self.logger.debug("First results are out. Looking out for circles and correcting.") | ||||
|  | ||||
| @@ -563,12 +540,11 @@ class Optimizer: | ||||
|  | ||||
|             for circle in circles : | ||||
|                 A, b = self.prevent_circle(circle, L) | ||||
|                 prob += (pl.lpSum([A[0][j] * x[j] for j in range(L*L)]) == b[0]) | ||||
|                 prob += (pl.lpSum([A[1][j] * x[j] for j in range(L*L)]) == b[1]) | ||||
|             prob.solve(pl.PULP_CBC_CMD(msg=False)) | ||||
|              | ||||
|             self.prob.solve(pl.PULP_CBC_CMD(msg=False)) | ||||
|  | ||||
|             status = pl.LpStatus[prob.status] | ||||
|             solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1) | ||||
|             status = pl.LpStatus[self.prob.status] | ||||
|             solution = [pl.value(var) for var in self.x]  # The values of the decision variables (will be 0 or 1) | ||||
|  | ||||
|             if status != 'Optimal' : | ||||
|                 self.logger.error("The problem is overconstrained, no solution after {i} cycles.") | ||||
| @@ -589,5 +565,5 @@ class Optimizer: | ||||
|         order = self.get_order(solution) | ||||
|         tour =  [landmarks[i] for i in order]  | ||||
|  | ||||
|         self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(prob.objective))}") | ||||
|         self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(self.prob.objective))}") | ||||
|         return tour | ||||
|   | ||||
		Reference in New Issue
	
	Block a user