Skip to content

Commit

Permalink
Merge pull request #88 from BrunoRosendo/refactor/cvrp-rpp-common-class
Browse files Browse the repository at this point in the history
Refactored step-based models into base class
  • Loading branch information
BrunoRosendo authored May 14, 2024
2 parents cd83459 + f86cafa commit ebf7f76
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 168 deletions.
144 changes: 144 additions & 0 deletions src/model/qubo/StepQuboVRP.py
Original file line number Diff line number Diff line change
@@ -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
]
97 changes: 19 additions & 78 deletions src/model/qubo/cvrp/MultiCVRP.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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:
"""
Expand All @@ -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)
)
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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.
Expand Down
Loading

0 comments on commit ebf7f76

Please sign in to comment.