diff --git a/src/model/qubo/StepQuboVRP.py b/src/model/qubo/StepQuboVRP.py new file mode 100644 index 0000000..a2791b1 --- /dev/null +++ b/src/model/qubo/StepQuboVRP.py @@ -0,0 +1,144 @@ +from abc import ABC, abstractmethod + +import numpy as np +from docplex.mp.dvar import Var + +from src.model.qubo.QuboVRP import QuboVRP + + +class StepQuboVRP(QuboVRP, ABC): + """ + A class to represent a QUBO math formulation of the step-based CVRP model. + This model should always be simplified, since some constraints assume the simplification. + + Attributes: + num_steps (int): Number of max steps for each vehicle. + num_used_locations (int): Number of locations used in the problem, including the auxiliary depot if used. + epsilon (int): Small value to avoid division by zero. + normalization_factor (int): Value to normalize the objective function. + """ + + def __init__( + self, + num_vehicles: int, + trips: list[tuple[int, int, int]], + distance_matrix: list[list[int]], + locations: list[tuple[int, int]], + use_deliveries: bool, + depot: int | None = 0, + ): + self.distance_matrix = distance_matrix + self.num_steps = self.get_num_steps() + self.num_used_locations = self.get_num_used_locations() + self.epsilon = 0.0001 + self.normalization_factor = np.max(self.distance_matrix) + self.epsilon + + super().__init__( + num_vehicles, trips, distance_matrix, locations, use_deliveries, True, depot + ) + + def create_vars(self): + """ + Create the variables for the VRP model. + """ + + self.x.extend( + self.model.binary_var(self.get_var_name(k, i, s)) + for k in range(self.num_vehicles) + for i in range(self.num_used_locations) + for s in range(self.num_steps) + ) + + def create_constraints(self): + """ + Create the constraints for the VRP model. + """ + + self.create_location_constraints() + self.create_vehicle_constraints() + + def create_location_constraints(self): + """ + Create the constraints that ensure each location is visited exactly once. + """ + + self.constraints.extend( + self.model.sum( + self.x_var(k, i, s) + for k in range(self.num_vehicles) + for s in range(self.num_steps) + ) + == 1 + for i in range(1, self.num_used_locations) + ) + + @abstractmethod + def create_vehicle_constraints(self): + """ + Create the vehicle constraints, depending on the model. + """ + pass + + @abstractmethod + def get_num_steps(self): + """ + Get the number of steps for the model. + """ + pass + + @abstractmethod + def get_num_used_locations(self): + """ + Get the number of used locations for the model. + """ + pass + + def get_result_route_starts(self, var_dict: dict[str, float]) -> list[int]: + """ + Get the starting location for each route from the variable dictionary. + """ + + route_starts = [] + + for k in range(self.num_vehicles): + for s in range(self.num_steps): + if self.get_var(var_dict, k, 0, s) == 0.0: + start = self.get_result_location(var_dict, k, s) + route_starts.append(start) + break + + return route_starts + + def get_result_next_location( + self, var_dict: dict[str, float], cur_location: int + ) -> int | None: + """ + Get the next location for a route from the variable dictionary. + """ + for k in range(self.num_vehicles): + for s in range(self.num_steps - 1): + if self.get_var(var_dict, k, cur_location, s) == 1.0: + return self.get_result_location(var_dict, k, s + 1) + + return None + + @abstractmethod + def get_result_location( + self, var_dict: dict[str, float], k: int, s: int + ) -> int | None: + """ + Get the location for a vehicle at a given step. + """ + pass + + def get_var_name(self, k: int, i: int, s: int | None = None) -> str: + """ + Get the name of a variable. + """ + + return f"x_{k}_{i}_{s}" + + def x_var(self, k: int, i: int, s: int) -> Var: + return self.x[ + k * self.num_used_locations * self.num_steps + i * self.num_steps + s + ] diff --git a/src/model/qubo/cvrp/MultiCVRP.py b/src/model/qubo/cvrp/MultiCVRP.py index 42e94d6..0f376e8 100644 --- a/src/model/qubo/cvrp/MultiCVRP.py +++ b/src/model/qubo/cvrp/MultiCVRP.py @@ -1,11 +1,9 @@ -import numpy as np -from docplex.mp.dvar import Var from docplex.mp.linear import LinearExpr -from src.model.qubo.QuboVRP import QuboVRP +from src.model.qubo.StepQuboVRP import StepQuboVRP -class MultiCVRP(QuboVRP): +class MultiCVRP(StepQuboVRP): """ A class to represent a QUBO math formulation of the CVRP model with all vehicles having different capacities. @@ -23,26 +21,8 @@ def __init__( locations: list[tuple[int, int]], ): self.capacities = capacities - self.num_steps = len(distance_matrix) + 1 - - self.epsilon = 0 - self.normalization_factor = np.max(distance_matrix) + self.epsilon - - self.copy_vars = False - - super().__init__(num_vehicles, [], distance_matrix, locations, False, True) - - def create_vars(self): - """ - Create the variables for the CVRP model. - """ - - self.x.extend( - self.model.binary_var(self.get_var_name(k, i, s)) - for k in range(self.num_vehicles) - for i in range(self.num_locations) - for s in range(self.num_steps) - ) + self.num_locations = len(locations) + super().__init__(num_vehicles, [], distance_matrix, locations, False) def create_objective(self) -> LinearExpr: """ @@ -55,42 +35,27 @@ def create_objective(self) -> LinearExpr: * self.x_var(k, i, s) * self.x_var(k, j, s + 1) for k in range(self.num_vehicles) - for i in range(self.num_locations) - for j in range(self.num_locations) + for i in range(self.num_used_locations) + for j in range(self.num_used_locations) for s in range(self.num_steps - 1) ) def create_constraints(self): """ - Create the constraints for the CPLEX model. + Create the constraints for the CVRP model. """ - self.create_location_constraints() - self.create_vehicle_constraints() + super().create_constraints() self.create_capacity_constraints() - def create_location_constraints(self): - """ - Create the constraints that ensure each location is visited exactly once. - """ - - self.constraints.extend( - self.model.sum( - self.x_var(k, i, s) - for k in range(self.num_vehicles) - for s in range(self.num_steps) - ) - == 1 - for i in range(1, self.num_locations) - ) - def create_vehicle_constraints(self): """ Create the constraints that ensure each vehicle starts and ends at the depot. """ self.constraints.extend( - self.model.sum(self.x_var(k, i, s) for i in range(self.num_locations)) == 1 + self.model.sum(self.x_var(k, i, s) for i in range(self.num_used_locations)) + == 1 for k in range(self.num_vehicles) for s in range(self.num_steps) ) @@ -103,7 +68,7 @@ def create_capacity_constraints(self): self.constraints.extend( self.model.sum( self.get_location_demand(i) * self.x_var(k, i, s) - for i in range(1, self.num_locations) + for i in range(1, self.num_used_locations) for s in range(cur_step + 1) ) <= self.capacities[k] @@ -123,39 +88,25 @@ def get_simplified_variables(self) -> dict[str, int]: variables[self.get_var_name(k, 0, 0)] = 1 variables[self.get_var_name(k, 0, self.num_steps - 1)] = 1 - for i in range(1, self.num_locations): + for i in range(1, self.num_used_locations): variables[self.get_var_name(k, i, 0)] = 0 variables[self.get_var_name(k, i, self.num_steps - 1)] = 0 return variables - def get_result_route_starts(self, var_dict: dict[str, float]) -> list[int]: + def get_num_steps(self): """ - Get the starting location for each route from the variable dictionary. + Get the number of steps for the model. """ - route_starts = [] - - for k in range(self.num_vehicles): - for s in range(self.num_steps): - if self.get_var(var_dict, k, 0, s) == 0.0: - start = self.get_result_location(var_dict, k, s) - route_starts.append(start) - break - return route_starts + return len(self.distance_matrix) + 1 - def get_result_next_location( - self, var_dict: dict[str, float], cur_location: int - ) -> int | None: + def get_num_used_locations(self): """ - Get the next location for a route from the variable dictionary. + Get the number of used locations for the model. """ - for k in range(self.num_vehicles): - for s in range(self.num_steps - 1): - if self.get_var(var_dict, k, cur_location, s) == 1.0: - return self.get_result_location(var_dict, k, s + 1) - return None + return self.num_locations def get_result_location( self, var_dict: dict[str, float], k: int, s: int @@ -164,22 +115,12 @@ def get_result_location( Get the location for a vehicle at a given step. """ - for i in range(self.num_locations): + for i in range(self.num_used_locations): if self.get_var(var_dict, k, i, s) == 1.0: return i return None - def get_var_name(self, k: int, i: int, s: int | None = None) -> str: - """ - Get the name of a variable. - """ - - return f"x_{k}_{i}_{s}" - - def x_var(self, k: int, i: int, s: int) -> Var: - return self.x[k * self.num_locations * self.num_steps + i * self.num_steps + s] - def get_capacity(self) -> int | list[int] | None: """ Get the capacity of the vehicles. diff --git a/src/model/qubo/rpp/InfiniteRPP.py b/src/model/qubo/rpp/InfiniteRPP.py index 35b009e..ae9b3ea 100644 --- a/src/model/qubo/rpp/InfiniteRPP.py +++ b/src/model/qubo/rpp/InfiniteRPP.py @@ -1,9 +1,7 @@ -from docplex.mp.dvar import Var +from src.model.qubo.StepQuboVRP import StepQuboVRP -from src.model.qubo.QuboVRP import QuboVRP - -class InfiniteRPP(QuboVRP): +class InfiniteRPP(StepQuboVRP): """ A class to represent a QUBO math formulation of the RPP model with an infinite capacity. Note that this model assumes a starting point with no cost to the first node for each vehicle, @@ -13,9 +11,7 @@ class InfiniteRPP(QuboVRP): Attributes: num_trips (int): Number of trips to be made. - num_steps (int): Number of max steps for each vehicle. used_locations_indices (list): Indices of the locations used in the problem, based on trip requests. - num_used_locations (int): Number of locations used in the problem. """ def __init__( @@ -27,28 +23,10 @@ def __init__( ): self.trips = trips self.num_trips = len(trips) - self.num_steps = 2 * self.num_trips + 1 - self.used_locations_indices = self.get_used_locations() - self.num_used_locations = len(self.used_locations_indices) - - self.epsilon = 0 - self.normalization_factor = max(max(distance_matrix)) + self.epsilon super().__init__( - num_vehicles, self.trips, distance_matrix, locations, True, True, None - ) - - def create_vars(self): - """ - Create the variables for the RPP model. - """ - - self.x.extend( - self.model.binary_var(self.get_var_name(k, i, s)) - for k in range(self.num_vehicles) - for i in range(self.num_used_locations + 1) - for s in range(self.num_steps) + num_vehicles, self.trips, distance_matrix, locations, True, None ) def create_objective(self): @@ -65,8 +43,8 @@ def create_objective(self): * self.x_var(k, i, s) * self.x_var(k, j, s + 1) for k in range(self.num_vehicles) - for i in range(1, self.num_used_locations + 1) - for j in range(1, self.num_used_locations + 1) + for i in range(1, self.num_used_locations) + for j in range(1, self.num_used_locations) for s in range(self.num_steps - 1) ) @@ -74,7 +52,7 @@ def create_objective(self): return_to_start_penalty = self.model.sum( self.x_var(k, i, s) * self.x_var(k, 0, s + 1) for k in range(self.num_vehicles) - for i in range(1, self.num_used_locations + 1) + for i in range(1, self.num_used_locations) for s in range(self.num_steps - 1) ) @@ -98,29 +76,6 @@ def create_trip_incentive(self): for s2 in range(s1 + 1, self.num_steps) ) - def create_constraints(self): - """ - Create the constraints for the RPP model. - """ - - self.create_location_constraints() - self.create_vehicle_constraints() - - def create_location_constraints(self): - """ - Create the constraints that ensure each location is visited exactly once. - """ - - self.constraints.extend( - self.model.sum( - self.x_var(k, i, s) - for k in range(self.num_vehicles) - for s in range(self.num_steps) - ) - == 1 - for i in range(1, self.num_used_locations + 1) - ) - def create_vehicle_constraints(self): """ Create the constraints that ensure each vehicle can only be at one location at a time. @@ -129,9 +84,7 @@ def create_vehicle_constraints(self): final_step = self.num_steps - 1 self.constraints.extend( - self.model.sum( - self.x_var(k, i, s) for i in range(self.num_used_locations + 1) - ) + self.model.sum(self.x_var(k, i, s) for i in range(self.num_used_locations)) == 1 for k in range(self.num_vehicles) for s in range(final_step) @@ -140,8 +93,7 @@ def create_vehicle_constraints(self): # Half-hot constraint meant to reduce variables in the last step self.constraints.extend( self.model.sum( - self.x_var(k, i, final_step) - for i in range(1, self.num_used_locations + 1) + self.x_var(k, i, final_step) for i in range(1, self.num_used_locations) ) <= 1 for k in range(self.num_vehicles) @@ -157,7 +109,7 @@ def get_simplified_variables(self) -> dict[str, int]: for k in range(self.num_vehicles): # Vehicles start at their respective locations and not anywhere else variables[self.get_var_name(k, 0, 0)] = 1 - for i in range(1, self.num_used_locations + 1): + for i in range(1, self.num_used_locations): variables[self.get_var_name(k, i, 0)] = 0 # Vehicles can't be at the starting point at the last step (see paper) @@ -176,28 +128,26 @@ def get_simplified_variables(self) -> dict[str, int]: return variables - def get_used_locations(self) -> list[int]: + def get_num_steps(self): """ - Get the indices of the locations used in the problem. This helps reduce the number of variables. + Get the number of steps for the model. """ - return list({location for trip in self.trips for location in trip[:2]}) + return 2 * self.num_trips + 1 - def get_result_route_starts(self, var_dict: dict[str, float]) -> list[int]: + def get_num_used_locations(self): """ - Get the starting location for each route from the variable dictionary. + Get the number of used locations for the model. """ - route_starts = [] + return len(self.used_locations_indices) + 1 - for k in range(self.num_vehicles): - for s in range(self.num_steps): - if self.get_var(var_dict, k, 0, s) == 0.0: - start = self.get_result_location(var_dict, k, s) - route_starts.append(start) - break + def get_used_locations(self) -> list[int]: + """ + Get the indices of the locations used in the problem. This helps reduce the number of variables. + """ - return route_starts + return list({location for trip in self.trips for location in trip[:2]}) def get_result_next_location( self, var_dict: dict[str, float], cur_location: int @@ -206,14 +156,8 @@ def get_result_next_location( Get the next location for a route from the variable dictionary. """ - cplex_location = self.used_locations_indices.index(cur_location) + 1 - - for k in range(self.num_vehicles): - for s in range(self.num_steps - 1): - if self.get_var(var_dict, k, cplex_location, s) == 1.0: - return self.get_result_location(var_dict, k, s + 1) - - return None + original_location = self.used_locations_indices.index(cur_location) + 1 + return super().get_result_next_location(var_dict, original_location) def get_result_location( self, var_dict: dict[str, float], k: int, s: int @@ -251,15 +195,3 @@ def is_result_feasible(self, var_dict: dict[str, float]) -> bool: return False return True - - def get_var_name(self, k: int, i: int, s: int | None = None) -> str: - """ - Get the name of a variable. - """ - - return f"x_{k}_{i}_{s}" - - def x_var(self, k: int, i: int, s: int) -> Var: - return self.x[ - k * (self.num_used_locations + 1) * self.num_steps + i * self.num_steps + s - ]