Custom problem settings
Prototyping in Python
So far, the example developed in the previous sections is limited to the EVRP-TW-PR. However, the library is designed to be easily extensible to other problem settings. This requires implementing five interfaces:
VertexData: Holds the data associated with a vertex
ArcData: Holds the data associated with an arc
ForwardLabel: Holds the forward label of a vertex
BackwardLabel: Holds the backward label of a vertex
Evaluation: Implements the main labeling and evaluation logic
Label and data classes
Implementing VertexData, ArcData, and Label classes is straightforward:
class CVRPVertexData:
def __init__(self, demand: int):
self.demand = demand
class CVRPArcData:
def __init__(self, distance: float):
self.distance = distance
class CVRPForwardLabel:
def __init__(self, distance: float, load: float):
self.distance = distance
self.load = load
class CVRPBackwardLabel:
def __init__(self, distance: float, load: float):
self.distance = distance
self.load = load
Note
The example above effectively duplicates the code for CVRPForwardLabel and CVRPBackwardLabel. There is no requirement for distinct ForwardLabel and BackwardLabel classes, i.e., we could replace these with a single CVRPLabel class. For the sake of clarity, we keep them separate in this example.
The evaluation class
Having defined the data and label classes holding problem-specific data, we can now implement the evaluation class. RoutingBlocks provides two interfaces for this purpose (cf. Evaluation):
routingblocks.PyEvaluation: General evaluation class. Receives the full route, i.e., all concatenated route segments, for cost evaluation. This is the most general interfaceroutingblocks.PyConcatenationBasedEvaluation: Evaluation class for problems with constant time, i.e., concatenation-based, evaluation. Provided for convenience, i.e., to provide a simple, more efficient interface for these special cases
As constant-time evaluation is possible for the CVRP, we implement the routingblocks.PyConcatenationBasedEvaluation interface in the following:
# Add a type alias for conciseness. Note: This is not required.
CVRPLabel = Union[CVRPForwardLabel, CVRPBackwardLabel]
class CVRPEvaluation(rb.PyConcatenationBasedEvaluation):
def __init__(self, storage_capacity: float):
rb.PyConcatenationBasedEvaluation.__init__(self)
self._storage_capacity = storage_capacity
self.load_penalty = 1.
def _compute_cost(self, distance: float, load: float) -> float:
# Helper function to compute the cost of a label.
return distance + self.load_penalty * max(0., load - self._storage_capacity)
def compute_cost(self, label: CVRPLabel) -> float:
return self._compute_cost(label.distance, label.load)
def get_cost_components(self, label: CVRPLabel) -> List[float]:
return [label.distance, label.load]
def concatenate(self, fwd: CVRPForwardLabel, bwd: CVRPBackwardLabel, vertex: rb.Vertex) -> float:
return self._compute_cost(fwd.distance + bwd.distance, fwd.load + bwd.load)
def create_backward_label(self, vertex: rb.Vertex) -> CVRPBackwardLabel:
return CVRPBackwardLabel(0., 0.)
def create_forward_label(self, vertex: rb.Vertex) -> CVRPForwardLabel:
return CVRPForwardLabel(0., vertex.data.demand)
def is_feasible(self, label: CVRPLabel) -> bool:
return label.load <= self._storage_capacity
def propagate_backward(self, succ_label: CVRPBackwardLabel, succ_vertex: rb.Vertex,
vertex: rb.Vertex, arc: rb.Arc) -> CVRPBackwardLabel:
return CVRPBackwardLabel(succ_label.distance + arc.data.distance, succ_label.load + succ_vertex.data.demand)
def propagate_forward(self, pred_label: CVRPForwardLabel, pred_vertex: rb.Vertex,
vertex: rb.Vertex, arc: rb.Arc) -> CVRPForwardLabel:
return CVRPForwardLabel(pred_label.distance + arc.data.distance, pred_label.load + vertex.data.demand)
Warning
Calls to vertex.data and arc.data are not type-safe: they work only if the vertex and arc data types have been defined in python. This is a tradeoff between performance and safety.
Theses classes can now be used in place of the ones provided by out of the box. In fact, using the solver developed in the previous sections (source code), we can solve the CVRP by simply swapping the evaluation class and creating the corresponding CVRPData classes:
1def create_instance(serialized_vertices, serialized_arcs) -> rb.Instance:
2 instance_builder = rb.utility.InstanceBuilder()
3 # Create and register the vertices
4 for vertex in serialized_vertices:
5 # Create problem-specific data held by vertices
6 vertex_data = CVRPVertexData(vertex['demand'])
7 # Register the vertex depending on it's type
8 if vertex['Type'] == 'd':
9 instance_builder.set_depot(vertex['StringID'], vertex_data)
10 elif vertex['Type'] == 'c':
11 instance_builder.add_customer(vertex['StringID'], vertex_data)
12 else:
13 instance_builder.add_station(vertex['StringID'], vertex_data)
14
15 # Create and register the arcs
16 for (i, j), arc in serialized_arcs.items():
17 # Create problem-specific data held by arcs
18 arc_data = CVRPArcData(arc['distance'])
19 instance_builder.add_arc(i, j, arc_data)
20
21 # Create instance
22 return instance_builder.build()
23
24# ...
25
26def alns(instance: rb.Instance, vehicle_storage_capacity: float,
27 number_of_iterations: int = 100, min_vertex_removal_factor: float = 0.2,
28 max_vertex_removal_factor: float = 0.4):
29 evaluation = CVRPEvaluation(vehicle_storage_capacity)
30 evaluation.load_penalty = 1000.0
Warning
We recommend implementing a custom Evaluation class by extending the native RoutingBlocks library instead of providing a Python implementation for code used beyond prototyping.
Writing a native extension
Pure Python-based implementations of routingblocks.PyEvaluation, routingblocks.PyConcatenationBasedEvaluation, and routingblocks.Propagator classes suffer from a significant performance penalty. This is due to the fact that parts of the library provided in native code need to return control to python interpreter for every evaluation.
To avoid this, the library provides native extension interfaces for all of it’s runtime critical components.
We provide an example of how to port the CVRP example to native code here.
Specifically, the repository provides the necessary boilerplate code for building, dependency management, packaging, publishing, and installation of custom native extensions.
We ask users to consider publishing their native extensions on PyPI to make them available to the community.
The source code of routingblocks.adptw.Evaluation (native/src/ADPTWEvaluation.cpp), routingblocks.niftw.Evaluation (native/src/NIFTWEvaluation.cpp), routingblocks.adptw.FacilityPlacementOptimizer (native/include/routingblocks/ADPTWEvaluation.h), and routingblocks.niftw.FacilityPlacementOptimizer (native/include/routingblocks/NIFTWEvaluation.h) provides further examples.