在存在顺序约束的情况下,OR 工具路由无法找到 'trivial' 最佳解决方案
OR-tools routing fails to find 'trivial' optimal solution in the presence of order-constraints
为了更好地理解 or-tools 路由背后的约束编程,我创建了一个 depot 的玩具示例和配置为允许两条路由的其他 4 个节点。
想法是车辆从站点 0
行驶到 1
,然后选择 2
或 3
,继续行驶到 4
并且returns 到仓库 0
;车辆选择绿色或红色路径。我的实际问题比较复杂,有多辆车,但有类似的约束。
对于这个例子,我为成本创建了一个欧氏距离函数:
class Distances:
def __init__(self):
self.locations = [
[-1, 0], # source
[ 0, -1], # waypoint 1
[ 0, 1], # waypoint 2
[ 1, 0], # destination
]
def __len__(self):
return len(self.locations) + 1
def dist(self, x, y):
return int(10000 * math.sqrt(
(x[0] - y[0]) ** 2 +
(x[1] - y[1]) ** 2))
def __call__(self, i, j):
if i == 0 and j == 0:
return 0
if j == 0 or i == 0:
return 1 # very small distance between depot and non-depot, simulating 0
return self.dist(self.locations[i - 1], self.locations[j - 1])
distance = Distances()
和一个 l0 距离函数来约束顺序:
# l0-distance to add order constraints
class Order:
def __call__(self, i, j):
return 0 if i == j else 1
order = Order()
然后我创建模型并尝试解决这个问题:
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)
search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
search_parameters.time_limit_ms = 3000
routing = pywrapcp.RoutingModel(len(distance), 1)
routing.SetArcCostEvaluatorOfAllVehicles(distance)
routing.SetDepot(0)
solver = routing.solver()
routing.AddDimension(order, int(1e18), int(1e18), True, "order")
# Since `ALL_UNPERFORMED` is used, each node must be allowed inactive
order_dimension = routing.GetDimensionOrDie("order")
routing.AddDisjunction([1], int(1e10))
routing.AddDisjunction([2, 3], int(1e10))
routing.AddDisjunction([4], int(1e10))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
solver.AddConstraint(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
solver.AddConstraint(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
# routing.AddPickupAndDelivery(1, 2)
# routing.AddPickupAndDelivery(1, 3)
# routing.AddPickupAndDelivery(2, 4)
# routing.AddPickupAndDelivery(3, 4)
routing.CloseModelWithParameters(search_parameters)
assignment = routing.SolveWithParameters(search_parameters)
if assignment is not None:
print('cost: ' + str(assignment.ObjectiveValue()))
route = []
index = routing.Start(0)
while not routing.IsEnd(index):
route.append(routing.IndexToNode(index))
index = assignment.Value(routing.NextVar(index))
for node in route:
print(' - {:2d}'.format(node))
else:
print('nothing found')
所以 [1]
和 [4]
是允许 ALL_UNPERFORMED
第一个解决方案起作用的析取,而析取 [2, 3]
说明绿色或红色路径应该被选中。
有了这些析取,求解器找到了一个解决方案,但是如果我添加 2
和 3
应该在 1
之后和 4
之前访问,求解器会根本不访问 2
或 3
。为什么会这样?为什么求解器不能找到更优的路径 0 -> 1 -> 2/3 -> 4 -> 0
避免 [2,3]
的 int(1e10)
析取惩罚?
编辑:
通过删除顺序约束并添加到 Distance.__call__
来软约束顺序约束:
if (i == 2 or j == 1) or (i == 3 or j == 1) or (i == 4 or j == 2) or (i == 4 or j == 3):
return int(1e10)
为了惩罚不允许的订单,导致路线 0 -> 2 -> 1 -> 4 -> 0
。所以我想知道为什么 or-tools 不会交换 1
和 2
,即使在 search_parameters.local_search_operators
.
中明确启用 use_swap_active
和 use_relocate_neighbors
注意: 失败,因为它应该是:
if (i == 2 and j == 1) or (i == 3 and j == 1) or (i == 4 and j == 2) or (i == 4 and j == 3):
return int(1e10)
结论:search-space小,较好的解在返回解use_relocate_neighbors
附近,但or-tools没有找到。为什么?
所有代码:
import pandas
import os.path
import numpy
import math
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2
class Distances:
def __init__(self):
self.locations = [
[-1, 0], # source
[ 0, -1], # waypoint 1
[ 0, 1], # waypoint 2
[ 1, 0], # destination
]
def __len__(self):
return len(self.locations) + 1
def dist(self, x, y):
return int(10000 * math.sqrt(
(x[0] - y[0]) ** 2 +
(x[1] - y[1]) ** 2))
def __call__(self, i, j):
if i == 0 and j == 0:
return 0
if j == 0 or i == 0:
return 1 # very small distance between depot and non-depot, simulating 0
return self.dist(self.locations[i - 1], self.locations[j - 1])
distance = Distances()
# l0-distance to add order constraints
class Order:
def __call__(self, i, j):
return 0 if i == j else 1
order = Order()
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)
search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
search_parameters.time_limit_ms = 3000
routing = pywrapcp.RoutingModel(len(distance), 1)
routing.SetArcCostEvaluatorOfAllVehicles(distance)
routing.SetDepot(0)
solver = routing.solver()
routing.AddDimension(order, int(1e18), int(1e18), True, "order")
# Since `ALL_UNPERFORMED` is used, each node must be allowed inactive
order_dimension = routing.GetDimensionOrDie("order")
routing.AddDisjunction([1], int(1e10))
routing.AddDisjunction([2, 3], int(1e10))
routing.AddDisjunction([4], int(1e10))
solver.AddConstraint(
(routing.ActiveVar(2) == 0)
or
(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
)
solver.AddConstraint(
(routing.ActiveVar(3) == 0)
or
(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
)
solver.AddConstraint(
(routing.ActiveVar(2) == 0)
or
(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
)
solver.AddConstraint(
(routing.ActiveVar(3) == 0)
or
(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
)
# routing.AddPickupAndDelivery(1, 2)
# routing.AddPickupAndDelivery(1, 3)
# routing.AddPickupAndDelivery(2, 4)
# routing.AddPickupAndDelivery(3, 4)
routing.CloseModelWithParameters(search_parameters)
assignment = routing.SolveWithParameters(search_parameters)
if assignment is not None:
print('cost: ' + str(assignment.ObjectiveValue()))
route = []
index = routing.Start(0)
while not routing.IsEnd(index):
route.append(routing.IndexToNode(index))
index = assignment.Value(routing.NextVar(index))
for node in route:
print(' - {:2d}'.format(node))
else:
print('nothing found')
@furnon 在 github 上通过 github-问题列表回答了我的问题:https://github.com/google/or-tools/issues/252#issuecomment-249646587
首先,约束规划在更严格的约束下表现更好,我想有些东西会被彻底搜索。特别是,我必须限制顺序维度:
routing.AddDimension(order, int(1e18), int(1e18), True, "order")
通过
更好地约束
routing.AddDimension(order, len(distance) + 1 ,len(distance) + 1, True, "order")
随后,不需要检查 2
或 3
是否处于活动状态,因此我们可以像这样简化顺序约束:
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
solver.AddConstraint(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
solver.AddConstraint(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
正如我在内联版本中所做的那样,但在全代码版本中没有。现在返回一个可行解。
为了更好地理解 or-tools 路由背后的约束编程,我创建了一个 depot 的玩具示例和配置为允许两条路由的其他 4 个节点。
想法是车辆从站点 0
行驶到 1
,然后选择 2
或 3
,继续行驶到 4
并且returns 到仓库 0
;车辆选择绿色或红色路径。我的实际问题比较复杂,有多辆车,但有类似的约束。
对于这个例子,我为成本创建了一个欧氏距离函数:
class Distances:
def __init__(self):
self.locations = [
[-1, 0], # source
[ 0, -1], # waypoint 1
[ 0, 1], # waypoint 2
[ 1, 0], # destination
]
def __len__(self):
return len(self.locations) + 1
def dist(self, x, y):
return int(10000 * math.sqrt(
(x[0] - y[0]) ** 2 +
(x[1] - y[1]) ** 2))
def __call__(self, i, j):
if i == 0 and j == 0:
return 0
if j == 0 or i == 0:
return 1 # very small distance between depot and non-depot, simulating 0
return self.dist(self.locations[i - 1], self.locations[j - 1])
distance = Distances()
和一个 l0 距离函数来约束顺序:
# l0-distance to add order constraints
class Order:
def __call__(self, i, j):
return 0 if i == j else 1
order = Order()
然后我创建模型并尝试解决这个问题:
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)
search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
search_parameters.time_limit_ms = 3000
routing = pywrapcp.RoutingModel(len(distance), 1)
routing.SetArcCostEvaluatorOfAllVehicles(distance)
routing.SetDepot(0)
solver = routing.solver()
routing.AddDimension(order, int(1e18), int(1e18), True, "order")
# Since `ALL_UNPERFORMED` is used, each node must be allowed inactive
order_dimension = routing.GetDimensionOrDie("order")
routing.AddDisjunction([1], int(1e10))
routing.AddDisjunction([2, 3], int(1e10))
routing.AddDisjunction([4], int(1e10))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
solver.AddConstraint(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
solver.AddConstraint(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
# routing.AddPickupAndDelivery(1, 2)
# routing.AddPickupAndDelivery(1, 3)
# routing.AddPickupAndDelivery(2, 4)
# routing.AddPickupAndDelivery(3, 4)
routing.CloseModelWithParameters(search_parameters)
assignment = routing.SolveWithParameters(search_parameters)
if assignment is not None:
print('cost: ' + str(assignment.ObjectiveValue()))
route = []
index = routing.Start(0)
while not routing.IsEnd(index):
route.append(routing.IndexToNode(index))
index = assignment.Value(routing.NextVar(index))
for node in route:
print(' - {:2d}'.format(node))
else:
print('nothing found')
所以 [1]
和 [4]
是允许 ALL_UNPERFORMED
第一个解决方案起作用的析取,而析取 [2, 3]
说明绿色或红色路径应该被选中。
有了这些析取,求解器找到了一个解决方案,但是如果我添加 2
和 3
应该在 1
之后和 4
之前访问,求解器会根本不访问 2
或 3
。为什么会这样?为什么求解器不能找到更优的路径 0 -> 1 -> 2/3 -> 4 -> 0
避免 [2,3]
的 int(1e10)
析取惩罚?
编辑:
通过删除顺序约束并添加到 Distance.__call__
来软约束顺序约束:
if (i == 2 or j == 1) or (i == 3 or j == 1) or (i == 4 or j == 2) or (i == 4 or j == 3):
return int(1e10)
为了惩罚不允许的订单,导致路线 0 -> 2 -> 1 -> 4 -> 0
。所以我想知道为什么 or-tools 不会交换 1
和 2
,即使在 search_parameters.local_search_operators
.
use_swap_active
和 use_relocate_neighbors
注意: 失败,因为它应该是:
if (i == 2 and j == 1) or (i == 3 and j == 1) or (i == 4 and j == 2) or (i == 4 and j == 3):
return int(1e10)
结论:search-space小,较好的解在返回解use_relocate_neighbors
附近,但or-tools没有找到。为什么?
所有代码:
import pandas
import os.path
import numpy
import math
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2
class Distances:
def __init__(self):
self.locations = [
[-1, 0], # source
[ 0, -1], # waypoint 1
[ 0, 1], # waypoint 2
[ 1, 0], # destination
]
def __len__(self):
return len(self.locations) + 1
def dist(self, x, y):
return int(10000 * math.sqrt(
(x[0] - y[0]) ** 2 +
(x[1] - y[1]) ** 2))
def __call__(self, i, j):
if i == 0 and j == 0:
return 0
if j == 0 or i == 0:
return 1 # very small distance between depot and non-depot, simulating 0
return self.dist(self.locations[i - 1], self.locations[j - 1])
distance = Distances()
# l0-distance to add order constraints
class Order:
def __call__(self, i, j):
return 0 if i == j else 1
order = Order()
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)
search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
search_parameters.time_limit_ms = 3000
routing = pywrapcp.RoutingModel(len(distance), 1)
routing.SetArcCostEvaluatorOfAllVehicles(distance)
routing.SetDepot(0)
solver = routing.solver()
routing.AddDimension(order, int(1e18), int(1e18), True, "order")
# Since `ALL_UNPERFORMED` is used, each node must be allowed inactive
order_dimension = routing.GetDimensionOrDie("order")
routing.AddDisjunction([1], int(1e10))
routing.AddDisjunction([2, 3], int(1e10))
routing.AddDisjunction([4], int(1e10))
solver.AddConstraint(
(routing.ActiveVar(2) == 0)
or
(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
)
solver.AddConstraint(
(routing.ActiveVar(3) == 0)
or
(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
)
solver.AddConstraint(
(routing.ActiveVar(2) == 0)
or
(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
)
solver.AddConstraint(
(routing.ActiveVar(3) == 0)
or
(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
)
# routing.AddPickupAndDelivery(1, 2)
# routing.AddPickupAndDelivery(1, 3)
# routing.AddPickupAndDelivery(2, 4)
# routing.AddPickupAndDelivery(3, 4)
routing.CloseModelWithParameters(search_parameters)
assignment = routing.SolveWithParameters(search_parameters)
if assignment is not None:
print('cost: ' + str(assignment.ObjectiveValue()))
route = []
index = routing.Start(0)
while not routing.IsEnd(index):
route.append(routing.IndexToNode(index))
index = assignment.Value(routing.NextVar(index))
for node in route:
print(' - {:2d}'.format(node))
else:
print('nothing found')
@furnon 在 github 上通过 github-问题列表回答了我的问题:https://github.com/google/or-tools/issues/252#issuecomment-249646587
首先,约束规划在更严格的约束下表现更好,我想有些东西会被彻底搜索。特别是,我必须限制顺序维度:
routing.AddDimension(order, int(1e18), int(1e18), True, "order")
通过
更好地约束routing.AddDimension(order, len(distance) + 1 ,len(distance) + 1, True, "order")
随后,不需要检查 2
或 3
是否处于活动状态,因此我们可以像这样简化顺序约束:
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
solver.AddConstraint(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
solver.AddConstraint(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
正如我在内联版本中所做的那样,但在全代码版本中没有。现在返回一个可行解。