用Google OR-Tools搭建简单车辆路线规划问题

一个用开源google orTools实现的有时间窗、容量限制、可设置接送对(pickup and delivery pair)、支持多路径和灵活起始地点的路径优化问题(CVRPTW: constraint vehicle routing problem with time windows)。程序使用开源求解器和建模工具,支持excel或txt表格形式的标准化输入和输出。

Google optimization tools不仅用C++语言开发了自己的求解器,而且可以通过接口调用几个常用的商用或开源求解器。项目使用了orTools的python接口,虽然功能上只实现了C++的部分功能,但是仍然可以满足项目要求。

对于CVRP问题来说,orTools的优点在于求解器以外又多了一层封装,使开发人员可以更方便地传入对应的参数,省略了很多约束建模的步骤。而且google求解器的工作方式是通过callback函数,相对于pyomo的AML方式,在描述非线性问题建模时更方便。

业务需求示例

业务每天有n辆车(n位员工)分别从k个集散中心出发,一天内共需要拜访m个客户站点。在一天结束时所有车辆/员工必须回到出发地。

每辆车有固定费用,以及最大容量、最多客户站点数、最大里程数、最高行驶速度的限制。客户有开放时间窗,对客户的拜访必须在允许的时间窗内完成。在各个客户站点有定义不同的工作时长。

每辆车/每位员工和每个集散中心都有工作开始和结束时间限制。对于车辆/员工来说,从集散中心出发和返回的时间必须在工作时间以内;对于集散中心来说,所有车辆的触发和返回必须在集散中心的开放时间以内。

目标是在满足所有约束的情况下,为每辆车安排一天的行程,使当天能够拜访的客户站点数达到最大值,所有车辆的成本总和最小。

车辆路线规划类问题描述

以TSP(travelling salesman problem)为基础,泛化而来的问题是车辆路线规划问题(VRP)。一种常见的VRP问题增加了车辆容量和站点访问时间窗口,是一种特殊的VRP问题,缩写为CVRPTW(constraint vehicle routing problem with time windows)。

TSP的混合整数线性优化描述:

假设集合V有n个站点 v 0 v_0 v0 v n v_n vn。如果有起始地点depot的集合 V d = v 0 , v 1 , … , v d V_d = {v_0, v_1, \dots, v_d} Vd=v0,v1,,vd,则

V ’ = V \ { V d } \mathbf V’ = \mathbf V \backslash \big\{ V_{d} \big\} V=V\{Vd} 代表客户站点。

站点间的连接为
x i j = { 1 站点i和j之间有连接 0 无连接 x_{ij} = \begin{cases} 1 & \quad \text{站点i和j之间有连接} \\ 0 & \quad \text{无连接} \end{cases} xij={10站点ij之间有连接无连接

目标函数为:

m i n i m i z e ∑ i = 1 n ∑ j ≠ i , j = 1 n c i j x i j , i , j ∈ V ( 1 ) minimize \sum_{i=1}^n \sum_{j \neq i,j=1}^n c_{ij}x_{ij}, \quad i,j \in \mathbf V \quad (1) minimizei=1nj̸=i,j=1ncijxij,i,jV(1)
c i j c_{ij} cij 是节点间的距离,depot的集合为空。

约束为:

0 ≤ x i j ≤ 1 ( 2 ) 0 \leq x_{ij} \leq 1 \quad (2) 0xij1(2)

c i j = c j i , i , j ∈ V ( 3 ) c_{ij} = c_{ji}, \quad i, j \in \mathbf V \quad (3) cij=cji,i,jV(3),所有路径上的cost都是对称的,

∑ i = 1 , i ≠ j n x i j = 1 , j ∈ V ( 4 ) \sum_{i=1,i \neq j}^n x_{ij} =1, \quad j \in \mathbf{V} \quad (4) i=1,i̸=jnxij=1,jV(4),从任意节点出发,只连接其他节点中的一个

∑ j = 1 , j ≠ i n x i j = 1 , i ∈ V ( 5 ) \sum_{j=1, j \neq i}^n x_{ij}=1,\quad i \in \mathbf{V} \quad (5) j=1,j̸=inxij=1,iV(5),到任意节点的路线,只从其他节点中的一个出发

u i ∈ Z , i ∈ V ( 6 ) u_i \in \mathbf Z,\quad i \in \mathbf{V} \quad (6) uiZ,iV(6)

u i − u j + n x i j ≤ n − 1 ( 7 ) u_i - u_j + nx_{ij} \leq n-1 \quad (7) uiuj+nxijn1(7)

0 ≤ u i , u j ≤ n − 1 , 2 ≤ i ≠ j ≤ n ( 8 ) 0 \leq u_i, u_j \leq n-1, \quad 2 \leq i \neq j \leq n \quad (8) 0ui,ujn1,2i̸=jn(8),保证经过所有节点的线路只有一条,而不是分开的很多条。具体见维基百科

CVRP的描述:

(1)、(2)、(3)、(4)、(5) 不变,增加约束:

∑ i n x i 0 = K ( 9 ) \sum_{i}^n x_{i0} = K \quad (9) inxi0=K(9)

∑ j n x 0 j = K ( 10 ) \sum_{j}^n x_{0j} = K \quad (10) jnx0j=K(10),离开和进入集散中心(depot)的车辆数相等,且等于车辆总数K。

设每辆车的路线为 R k = { v 0 , v 1 , … , v m + 1 } , v i ∈ V , v 0 = v m + 1 R_k = \big\{ v_0, v_1, \dots, v_{m+1} \big\}, \quad v_i \in \mathbf V, v_0 = v_{m+1} Rk={v0,v1,,vm+1},viV,v0=vm+1 v 0 , v m + 1 ∈ V d , k ∈ K v_0, v_{m+1} \in \mathbf V_d, k \in K v0,vm+1Vd,kK

另外设每辆车分别有:

  • 驾驶时间上限 t i m e C o n s t r a i n t k timeConstraint_k timeConstraintk
  • 驾驶路程上限 d i s t a n c e C o n s t r a i n t k distanceConstraint_k distanceConstraintk
  • 容量上限 c a p a c i t y C o n s t r a i n t k capacityConstraint_k capacityConstraintk
  • 拜访站点数上限 n r S i t e C o n s t r a i n t k nrSiteConstraint_k nrSiteConstraintk
  • 平均驾驶速度 s p e e d k speed_k speedk

设每个客户站点的需求量为 q i q_i qi,所需服务时间为 δ i \delta_i δi

时间约束(time cost) T C ( R k ) = ∑ i = 0 m t c v i , v i + 1 + ∑ i = 1 m δ v i ≤ t i m e C o n s t r a i n t k , k ∈ K ( 11 ) TC(R_k) = \sum_{i=0}^m tc_{v_i,v_{i+1}} + \sum_{i=1}^m \delta_{v_i} \leq timeConstraint_k, \quad k \in K \quad (11) TC(Rk)=i=0mtcvi,vi+1+i=1mδvitimeConstraintk,kK(11)

其中 t c i j = c i j s p e e d k , i , j ∈ V , k ∈ K tc_{ij} = \cfrac{c_{ij}}{speed_k} , i, j \in \mathbf V, k \in K tcij=speedkcij,i,jV,kK

距离约束(distance cost) D C ( R k ) = ∑ i = 0 m c v i , v i + 1 ≤ d i s t a n c e C o n s t r a i n t k , ( 12 ) DC(R_k) = \sum_{i=0}^m c_{v_i, v_{i+1}} \leq distanceConstraint_k, \quad (12) DC(Rk)=i=0mcvi,vi+1distanceConstraintk,(12)

容量约束(capacity cost) C C ( R k ) = ∑ i = 1 m q v i ≤ c a p a c i t y C o n s t r a i n t k , ( 13 ) CC(R_k) = \sum_{i=1}^m q_{v_i} \leq capacityConstraint_k, \quad (13) CC(Rk)=i=1mqvicapacityConstraintk,(13)

站点数约束(number of site cost) N S C ( R k ) = ∣ R k ∣ − 2 ≤ n r S i t e C o n s t r a i n t k , ( 14 ) NSC(R_k) = |R_k| - 2 \leq nrSiteConstraint_k, \quad (14) NSC(Rk)=Rk2nrSiteConstraintk,(14)

另外考虑计算效率,设K的下限为

b ( V ) = m a x i m u m ( L B c , L B s ) b(V) = maximum(LB_{c}, LB_{s}) b(V)=maximum(LBc,LBs)

其中

L B c = ∑ i ∈ V ′ q i m a x k ( c a p a c i t y C o n s t r a i n t s k ) LB_c = \cfrac{\sum_{i \in \mathbf V'} q_{i}}{max_k(capacityConstraints_k)} LBc=maxk(capacityConstraintsk)iVqi

L B s = ∣ V ′ ∣ m a x k ( n r S i t e C o n s t r a i n t s k ) LB_s = \cfrac{|\mathbf V'|}{max_k(nrSiteConstraints_k)} LBs=maxk(nrSiteConstraintsk)V

CVRPTW的描述:

如果考虑客户站点的时间窗口(TwStart, TwEnd):

T w S t a r t i + δ i ≤ T w E n d i , i ∈ V ′ ( 15 ) TwStart_i + \delta_i \leq TwEnd_i, \quad i \in \mathbf V' \quad (15) TwStarti+δiTwEndi,iV(15)

到达站点 v i v_i vi 的时间

A r v k , i = ∑ j = 0 i − 1 t c v j , v j + 1 + ∑ j = 1 i − 1 δ v j ≥ T w S t a r t v i , v i , v j ∈ R k ( 16 ) Arv_{k, i} = \sum_{j=0}^{i-1} tc_{v_j, v_{j+1}} + \sum_{j=1}^{i-1} \delta_{v_j} \geq TwStart_{v_i}, \quad v_i, v_j \in R_k \quad (16) Arvk,i=j=0i1tcvj,vj+1+j=1i1δvjTwStartvi,vi,vjRk(16)

离开站点 v i v_i vi 的时间

D e p k , i = A r v k , i + δ v i ≤ T w E n d v i , v i ∈ R k ( 17 ) Dep_{k, i} = Arv_{k, i} + \delta_{v_i} \leq TwEnd{v_i}, \quad v_i \in R_k \quad (17) Depk,i=Arvk,i+δviTwEndvi,viRk(17)

具体代码实现:

orTools里cost的计算是通过callback函数实现的,函数的格式是function(from_node_index, to_node_index)。在生成约束的时候把函数直接作为变量传递给orTools的类。

计算任意两个节点间距离的函数:

class CreateDistanceEvaluator():
    def __init__(self, locationData, multiplier=1.3):
        self.distances = {}
        self._multiplier = multiplier 
        
        for from_node in np.arange(len(locationData)):
            self.distances[from_node] = {}
            for to_node in np.arange(len(locationData)):
                if np.alltrue(np.equal(from_node, to_node)):
                    self.distances[from_node][to_node] = 0
                else:
                    self.distances[from_node][to_node] = convertToInt(
                            self.getDistanceInKm(locationData[from_node],
                            locationData[to_node]) * self._multiplier)
    
    @staticmethod
    def getDistanceInKm(coord1,coord2):
        # https://en.wikipedia.org/wiki/Haversine_formula
        lat1, lon1 = coord1
        lat2, lon2 = coord2
        
        if np.isnan(lat1 * lon1 * lat2 * lon2):
            return 0
        
        def deg2rad(deg):
            return deg * (math.pi / 180)
        
        R = 6371  # 地球半径(公里)
        dLat = deg2rad(lat2-lat1)  
        dLon = deg2rad(lon2-lon1); 
        a = (   math.sin(dLat/2) * math.sin(dLat/2) 
                + math.cos(deg2rad(lat1)) * math.cos(deg2rad(lat2)) * 
                  math.sin(dLon/2) * math.sin(dLon/2)    )
    
        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) 
        d = R * c 
        return d
    
    def distance_evaluator(self, from_node, to_node):
        """
        callback函数。在计算cost的过程中直接抽取预先计算的距离值,提高速度。
        """
        return self.distances[from_node][to_node]

计算节点需求的callback函数,也用来计算访问节点数:

class CreateDemandEvaluator():
    def __init__(self, demandData):
        self._demands = demandData
        
    def demand_evaluator(self, from_node, to_node):
        """
        Callback函数
        """
        del to_node
        return self._demands[from_node]

计算时间cost的callback函数。

由于每辆车的速度不同,计算cost的方式有区别,需要生成一组不同的evaluator类。

class CreateAllTransitEvaluators():
    def __init__(self, vehicles, distances, serviceTimes):
        """
        callback函数list
        """
        self._vehicles = vehicles
        self._distances = distances
        self._serviceTimes = serviceTimes
        self.evaluators = []
        # 每辆车根据速度不同单独生成对应的evaluator:
        for v in vehicles.speeds:
            evaluator = CreateOneTransitEvaluator(v, self._distances, 
                                self._serviceTimes).one_transit_evaluator
            self.evaluators.append(evaluator)
class CreateOneTransitEvaluator():
    def __init__(self, speed, distances, serviceTimes):
        self._speed = speed
        self._distances = distances
        self._serviceTimes = serviceTimes
        
    def one_transit_evaluator(self, from_node, to_node):
        """
        单一callback函数:
        计算单个节点的时间总量 = 当前节点到下一节点的距离 / 车辆速度 + 当前节点的服务时长
        """
        if from_node == to_node:
            return 0
        if self._speed == 0:
            return sys.maxsize
        return convertToInt(self._distances[from_node][to_node] / self._speed 
                + self._serviceTimes[from_node])

定义约束:RoutingModel类的接口定义

其中AddDimension(evaluator, slack, capacity, *args)用来定义约束,常用有4种:

  • AddDimension(singleEvaluator, intSlack, intCapacity, *args): 所有车辆共用callback函数
    evaluator 和约束上限capacity。
  • AddDimensionWithVehicleCapacity(singleEvaluator, intSlack, listCapacity, *args): 所有车辆
    共用evaluator,但使用自定义的约束上限capacity,入参格式为list。比如:每辆车的容量
    不同。
  • AddDimensionWithVehicleTransits(listEvaluator, intSlack, intCapacity, *args): 所有车辆共用
    约束上限capacity,但使用自定义的cost计算方式,入参格式为evaluator list, 对应不同车辆。
    比如:每辆车的平均驾驶速度不同,计算路途时间需要不同的evaluator。
  • AddDimensionWithVehicleTransitAndCapacity(listEvaluator, intSlack, listCapacity, *args):
    cost计算方式和约束上限都可以用每辆车的自定义方式。

另外:

  • 第二项int 参数slack的定义规定了约束变量的松弛范围。
  • 第四项boolean 参数fix_start_cumul_to_zero定义了cost是否默认从0开始计数。当涉及时间窗口,time cost的起始点大于0,这一项需要设置为false.
  • 第五项string 参数name为这个约束维度命名,方便在优化结果中提取约束相关的各项指标。
def add_capacity_constraints(routing, listOfConstraints, evaluator, varName):
    
    name = varName
    routing.AddDimensionWithVehicleCapacity(
           evaluator, 
           0, 
           listOfConstraints, 
           True, 
           name)
def add_transit_and_capacity_constraints(routing, listOfConstraints, 
                                         listOfEvaluators, intSlack, varName):
    name = varName
    routing.AddDimensionWithVehicleTransitAndCapacity(
        listOfEvaluators,
        intSlack,
        listOfConstraints,
        False,
        name)

定义时间窗口约束:

RoutingModel.GetDimensionOrDie函数根据给定的约束维度名称提取对应的约束信息。

从dimension里返回的值有几类:

  • transit变量:当前节点的增加或减少值,比如时间约束里就是这个节点耗费的服务时间和路途时间
  • cumulative 变量:从开始节点到当前节点的累加量,比如时间约束里就是车辆到达这个节点以前耗费的总时长。
  • slack变量

时间窗口的约束就是加在每个cumulative 变量上的,所以需要首先调用RoutingModel.GetDimensionOrDie获取CumulVar的值。

另外注意几点细节:

  • 数据的顺序角标有两种,一种是输入顺序,对应第一章里以i 标识的变量,在程序中是node_idx和veh_idx;另外一种是在每个路线中的顺序,对应第一章里以 v i v_i vi 标识的变量,在程序中是index变量。而函数NodeToIndex就是从自然顺序到路线顺序的lookup过程。
  • orTools里RoutingModel.NodeToIndex和RoutingModel.Start/End 函数的区别:所有代表客户站点的节点(就是路线中的非起始或终止节点)可以直接通过NodeToIndex获取;而所有的起始和终止节点由于可能被多个车辆引用,所以在分配节点编号时并不对应自然顺序。要获取这些节点,必须用Start或End函数。
  • RoutingModel.AddToAssignment的作用:assignment指经过求解器solve的路径规划结果。通过assignment的各种函数(比如Value, Min, Max)可以获取模型变量的取值或取值范围。对于约束维度来说,slackVar变量默认并不保存在最终的assignment里,所以如果想在最终结果里看到时间变量的松弛范围,需要在约束建模时调用AddToAssignment(SlackVar)
  • 任何约束维度的Slack变量都只存在于每段弧的起始节点上,所以如果试图对end节点调用SlackVar(RoutingModel.End(vehicle_id),程序会报错。
def add_timewindow_constraints(routing, data, varName='time'):
    
    time_dimension = routing.GetDimensionOrDie(varName)
    for node_idx, time_window in enumerate(data.timeWindows):
        if node_idx <= np.max(data.depotIndex):
            continue
        index = routing.NodeToIndex(node_idx)
        servTime = data.serviceTimes[node_idx]
        time_dimension.CumulVar(index).SetRange(
                                    time_window[0], time_window[1]-servTime)
        routing.AddToAssignment(time_dimension.SlackVar(index))
    for veh_idx in np.arange(data.nrVehicles):
        index = routing.Start(veh_idx)
        servTime = data.serviceTimes[data.depotIndex[veh_idx]]
        time_dimension.CumulVar(index).SetRange(
                                        data.earliestWorkHours[veh_idx],
                                        data.latestWorkHours[veh_idx]-servTime)
        routing.AddToAssignment(time_dimension.SlackVar(index))
    for veh_idx in np.arange(len(data.depotIndex)):
        index = routing.End(veh_idx)
        servTime = data.serviceTimes[data.depotIndex[veh_idx]]
        time_dimension.CumulVar(index).SetRange(
                                        data.earliestWorkHours[veh_idx],
                                        data.latestWorkHours[veh_idx]-servTime)

用ConsolePrinter类打印结果:主要通过调用GetDimensionOrDie函数获取各个约束维度的信息:

class ConsolePrinter():
    def __init__(self, data, routing, assignment, distances):
        self._data = data
        self._routing = routing
        self._assignment = assignment
        self._distances = distances
        
    def printAll(self):
        total_dist = 0
        total_siteCount = 0
        total_fulfilledDemand = 0
        capacity_dimension = self._routing.GetDimensionOrDie('capacity')
        distance_dimension = self._routing.GetDimensionOrDie('dailyDistance')
        time_dimension = self._routing.GetDimensionOrDie('time')
        siteCount_dimension = self._routing.GetDimensionOrDie('dailyNrJobs')
        
        for vehicle_id in np.arange(self._data.nrVehicles):
            index = self._routing.Start(vehicle_id)
            plan_output = 'Route for person {0}: \n'.format(vehicle_id)
            route_startTime = self._assignment.Value(time_dimension.CumulVar(index))
            route_serviceTime = 0
            route_timeWindow = []
            while not self._routing.IsEnd(index):
                node_index = self._routing.IndexToNode(index)
                next_node_index = self._routing.IndexToNode(
                        self._assignment.Value(self._routing.NextVar(index)))
                step_dist = self._distances[node_index][next_node_index]
                step_load = self._data.demands[node_index]
                step_serviceTime = self._data.serviceTimes[node_index]
                route_serviceTime += step_serviceTime
                step_timewindow = self._data.timeWindows[node_index]
                route_timeWindow.append(step_timewindow)
                time_var = time_dimension.CumulVar(index)
                time_min = self._assignment.Min(time_var)
                time_max = self._assignment.Max(time_var)
                slack_var = time_dimension.SlackVar(index)
                slack_min = self._assignment.Min(slack_var)
                slack_max = self._assignment.Max(slack_var)
                
                plan_output += (
                    ' {node} capacity({capa}) distance({dist}) serviceTime({minTime},{maxTime}) slack({minSlack},{maxSlack})->\n'
                    .format(node=node_index, capa=step_load, dist=step_dist, 
                        minTime=time_min, maxTime=time_max, minSlack=slack_min, 
                        maxSlack=slack_max) )
                index = self._assignment.Value(self._routing.NextVar(index))
            
            end_idx = self._routing.End(vehicle_id) 
            route_endTime = self._assignment.Value(time_dimension.CumulVar(end_idx)) 
            route_dist = self._assignment.Value(distance_dimension.CumulVar(end_idx))
            route_load = self._assignment.Value(capacity_dimension.CumulVar(end_idx))
            route_siteCount = self._assignment.Value(siteCount_dimension.CumulVar(end_idx))
            node_index = self._routing.IndexToNode(index)
            total_dist += route_dist
            total_siteCount += route_siteCount
            total_fulfilledDemand += route_load
            
            plan_output += ' {0} \n'.format(node_index)
            plan_output += ('Objective: minimize vehicle cost + distance cost, maximize number of sites visited\nConstraint:\n 1.vehicle capacity {load} pieces\n 2.vehicle daily distance {dailyDistance} km\n 3.vehicle daily sites {dailySites}\n 4.depot opening hours {depotTime} min\n 5.vehicle shift times {vehicleTime} min\n 6.location time windows {tw}\n'
                    .format(load=self._data.vehicles.capacity[vehicle_id],
                            depotTime=self._data.timeWindows[self._data.depotIndex[vehicle_id]],
                            tw=route_timeWindow,
                            dailyDistance = self._data.vehicles.dailyDistanceLimit[vehicle_id],
                            dailySites = self._data.vehicles.nrJobLimit[vehicle_id],
                            vehicleTime = self._data.vehicles.vehicleTimeWindows[vehicle_id]
                            ) )
            plan_output += 'Result:\n 1.load of the route: {0} pcs\n'.format(route_load)
            plan_output += ' 2.distance of the route: {0} km\n'.format(route_dist)
            plan_output += ' 3.visited nr. sits: {0}\n'.format(route_siteCount)
            plan_output += ' 4.timespan of the route: ({0},{1}) min\n'.format(
                                                                                    route_startTime, route_endTime)
            plan_output += '   of which service time: {0} min\n'.format(route_serviceTime)
            print(plan_output)

        print('Total distance of all routes: {0} km\nTotal nr. visited sites: {1}\nTotal fulfilled demand: {2}\n'
              .format(total_dist,total_siteCount,total_fulfilledDemand))
        print('Dropped nodes: {0}\n').format(self.getDropped())
    
    def getDropped(self):
        dropped = []
        for idx in np.arange(self._routing.Size()):
            if self._assignment.Value(self._routing.NextVar(idx)) == idx:
                dropped.append(idx)
        return dropped

在console里画出每条路线:这几个函数主要参考了google的案例代码:

def discrete_cmap(N, base_cmap=None):
    """
    based on https://github.com/google/or-tools/blob/master/examples/python/cvrptw_plot.py
    Create an N-bin discrete colormap from the specified input map
    """
    # Note that if base_cmap is a string or None, you can simply do
    #    return plt.cm.get_cmap(base_cmap, N)
    # The following works for string, None, or a colormap instance:

    base = plt.cm.get_cmap(base_cmap)
    color_list = base(np.linspace(0, 1, N))
    cmap_name = base.name + str(N)
    return base.from_list(cmap_name, color_list, N)

def build_vehicle_route(routing, assignment, locations, vehicle_id):
    """
    based on https://github.com/google/or-tools/blob/master/examples/python/cvrptw_plot.py
    Build a route for a vehicle by starting at the strat node and
    continuing to the end node.
    """
    
    veh_used = routing.IsVehicleUsed(assignment, vehicle_id)
    if veh_used:
        route = []
        node = routing.Start(vehicle_id)  # Get the starting node index
        route.append(locations[routing.IndexToNode(node)])
        while not routing.IsEnd(node):
          route.append(locations[routing.IndexToNode(node)])
          node = assignment.Value(routing.NextVar(node))
        
        route.append(locations[routing.IndexToNode(node)])
        return route
    else:
        return None


def plot_vehicle_routes(vehicle_routes, ax, data):
    """
    based on https://github.com/google/or-tools/blob/master/examples/python/cvrptw_plot.py
    Plot the vehicle routes on matplotlib axis ax1.
    """
    veh_used = [v for v in vehicle_routes if vehicle_routes[v] is not None]
    cmap = discrete_cmap(len(data.vehicles.names) + 2, 'nipy_spectral')

    for veh_number in veh_used:
        lats, lons = zip(*[(c[0], c[1]) for c in vehicle_routes[veh_number]])
        lats = np.array(lats)
        lons = np.array(lons)

        ax.plot(lons, lats, 'o', mfc=cmap(veh_number + 1))
        ax.quiver(
            lons[:-1],
            lats[:-1],
            lons[1:] - lons[:-1],
            lats[1:] - lats[:-1],
            scale_units='xy',
            angles='xy',
            scale=1,
            color=cmap(veh_number + 1))

在main函数中完成约束建模。运行结果见下:

# 使用默认的建模参数:
model_parameters = pywrapcp.RoutingModel.DefaultModelParameters()
routing = pywrapcp.RoutingModel(
        data.nrLocations, 
        data.nrVehicles, 
        data.depotIndex, 
        data.depotIndex,
        model_parameters)

# 添加车辆成本:
for n,v in enumerate(data.vehicles.costs):
    routing.SetFixedCostOfVehicle(v, n)

# 添加距离成本:
distEval = CreateDistanceEvaluator(data.locations)
distance_evaluator = distEval.distance_evaluator # callback函数
routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator)

# 添加每辆车的最大行驶距离约束:
add_capacity_constraints(routing, data.vehicles.dailyDistanceLimit,
                         distance_evaluator, 'dailyDistance')    

# 添加每辆车的最大容量约束:
demand_evaluator = CreateDemandEvaluator(data.demands).demand_evaluator
add_capacity_constraints(routing, data.vehicles.capacity, 
                         demand_evaluator, 'capacity')

# 添加每辆车的最多访问站点数约束:
nrJobs_evaluator = CreateDemandEvaluator(
                                data.visitedLocations).demand_evaluator
add_capacity_constraints(routing, data.vehicles.nrJobLimit,
                         nrJobs_evaluator, 'dailyNrJobs')

# 添加运营时间约束
transitEval = CreateAllTransitEvaluators(data.vehicles, distEval.distances, 
                                         data.serviceTimes)
add_transit_and_capacity_constraints(routing, data.latestWorkHours, 
                                 transitEval.evaluators, 
                                 int(np.max(data.latestWorkHours)), 'time')

# 添加时间窗口约束
add_timewindow_constraints(routing, data, 'time')

# 设置搜索策略(本例中主要使用了默认参数,其他参数参考google教程):
# https://developers.google.com/optimization/routing/routing_options
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
    routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)

# 设置惩罚项,允许有客户站点不被访问(当所有车辆达到约束上限时)
# 否则问题有可能无解。
non_depot = set(range(data.nrLocations))
non_depot.difference_update(data.depotIndex)
penalty = 400000
nodes = [routing.AddDisjunction([c], penalty) for c in non_depot]

# 求解
assignment = routing.SolveWithParameters(search_parameters)

# 打印结果
printer = ConsolePrinter(data, routing, assignment, distEval.distances)
printer.printAll()

# plot routes
dropped = []
dropped = printer.getDropped()

vehicle_routes = {}
for veh in np.arange(data.nrVehicles):
    vehicle_routes[veh] = build_vehicle_route(routing, assignment, 
                                                      data.locations, veh)
# Plotting of the routes in matplotlib.
fig = plt.figure()
ax = fig.add_subplot(111)
# Plot all the nodes as black dots.
clon, clat = zip(*[(c[0], c[1]) for i,c in enumerate(data.locations) if i not in dropped])
ax.plot(clat, clon, 'k.')
# plot the routes as arrows
plot_vehicle_routes(vehicle_routes, ax, data)

更多改进

高德地图提供API给前端开发者上传位置信息,可以在实际的地图中显示规划完成的路线。具体操作可以参考:https://cn.aliyun.com/jiaocheng/441387.html 。

你可能感兴趣的:(算法)