<代码分享> 一种无人机配合卡车配送的车辆路径规划模型

本文为本人博客《一种无人机配合卡车配送的车辆路径规划模型》的代码分享。
由于近期此文的关注者较多,代码分享较为不便,因此决定专门写一篇博客以提供源码。
感谢各位博友关注,本人能力有限,如有错误,还请及时批评指正!
原文链接:https://blog.csdn.net/Zhang_0702_China/article/details/115215219


代码文件结构


代码的文件结构如下:
  • config: 参数、输入数据(随机生成)
  • docs: 模型文档、参考文献(见前面原文链接
  • log: 运行日志
  • model: 模型核心模块
  • output: 输出文件,即可视化结果
  • utils: 工具模块,本文代码只涉及日志打印工具
  • visual: 可视化模块
  • main_tsp: TSP 模型主程序
  • main_vrp: VRP 模型主程序

<代码分享> 一种无人机配合卡车配送的车辆路径规划模型

具体代码


参数与输入数据


TSP:

from datetime import datetime
import random
import math

from utils.logger import log_tsp as log


dts = datetime.now()


# nodes
NUM_NODE = 50
NUM_NODE_NAV = 20
LIST_NODE_UAV = list(range(NUM_NODE - NUM_NODE_NAV + 1, NUM_NODE + 1))

# coordinates
random.seed(2021)
RANGE_COORDINATE = (0, 100)
LIST_COORDINATE = [(random.randint(RANGE_COORDINATE[0], RANGE_COORDINATE[1]),
                    random.randint(RANGE_COORDINATE[0], RANGE_COORDINATE[1])) for _ in range(NUM_NODE)]

# distance
ORIGIN = (round((RANGE_COORDINATE[1] - RANGE_COORDINATE[0]) / 2),
          round((RANGE_COORDINATE[1] - RANGE_COORDINATE[0]) / 2))
LIST_COORDINATE_ = [ORIGIN] + LIST_COORDINATE
MAT_DISTANCE = [[0.0 for _ in range(0, NUM_NODE + 1)] for _ in range(0, NUM_NODE + 1)]
range_wave = (0.8, 1.2)
for i in range(0, NUM_NODE + 1):
    for j in range(0, NUM_NODE + 1):
        distance = round(math.sqrt((LIST_COORDINATE_[i][0] - LIST_COORDINATE_[j][0]) ** 2
                                   + (LIST_COORDINATE_[i][1] - LIST_COORDINATE_[j][1]) ** 2)
                         * (range_wave[0] + random.random() * (range_wave[1] - range_wave[0])), 4)
        MAT_DISTANCE[i][j] = distance

# cost
COST_UAV = 0.2


dte = datetime.now()
tm = round((dte - dts).seconds + (dte - dts).microseconds / (10 ** 6), 3)
log.info(msg="random data generating time:  {} s".format(tm))

VRP:

from datetime import datetime
import random
import math

from utils.logger import log_vrp as log


dts = datetime.now()


# nodes
NUM_NODE = 50
NUM_NODE_NAV = 10
LIST_NODE_UAV = list(range(NUM_NODE - NUM_NODE_NAV + 1, NUM_NODE + 1))

# coordinates
random.seed(2021)
RANGE_COORDINATE = (0, 100)
LIST_COORDINATE = [(random.randint(RANGE_COORDINATE[0], RANGE_COORDINATE[1]),
                    random.randint(RANGE_COORDINATE[0], RANGE_COORDINATE[1])) for _ in range(NUM_NODE)]

# distance
ORIGIN = (round((RANGE_COORDINATE[1] - RANGE_COORDINATE[0]) / 2),
          round((RANGE_COORDINATE[1] - RANGE_COORDINATE[0]) / 2))
LIST_COORDINATE_ = [ORIGIN] + LIST_COORDINATE
MAT_DISTANCE = [[0.0 for _ in range(0, NUM_NODE + 1)] for _ in range(0, NUM_NODE + 1)]
range_wave = (0.8, 1.2)
for i in range(0, NUM_NODE + 1):
    for j in range(0, NUM_NODE + 1):
        distance = round(math.sqrt((LIST_COORDINATE_[i][0] - LIST_COORDINATE_[j][0]) ** 2
                                   + (LIST_COORDINATE_[i][1] - LIST_COORDINATE_[j][1]) ** 2)
                         * (range_wave[0] + random.random() * (range_wave[1] - range_wave[0])), 4)
        MAT_DISTANCE[i][j] = distance

# cost
COST_UAV = 0.1

# weight, load
upper_weight_item = 20
LIST_WEIGHT = [random.randint(0, upper_weight_item) for _ in range(NUM_NODE)]
UPPER_LOAD = 100
log.info(msg="total weight:  {}".format(sum(LIST_WEIGHT)))


dte = datetime.now()
tm = round((dte - dts).seconds + (dte - dts).microseconds / (10 ** 6), 3)
log.info(msg="random data generating time:  {} s".format(tm))

模型核心模块


在 TSP 模型中,通过 GG 模型的建模方式消除子回路;在 VRP 模型中,通过卡车载重约束的传递性消除子回路。
有关 TSP 模型的建模方式可见本人博客:https://blog.csdn.net/Zhang_0702_China/article/details/106983492

模型求解通过调用 Gurobi 求解器的方式实现,有关 Gurobi 求解器的 Python 调用,可见本人博客:https://blog.csdn.net/Zhang_0702_China/article/details/115520346

TSP:

from datetime import datetime
from typing import List, Dict, Tuple

from gurobipy import *

from utils.logger import log_tsp as log


def tsp_gg(num_node: int, list_node_uav: List[int], mat_distance: List[List[int]], cost_uav: float = 0.5) \
        -> Tuple[List[int], Dict[int, Tuple[int, int]]]:
    """
    TSP model, GG formulation

    :param num_node:  number of nodes
    :param list_node_uav:  nodes can be served by uav
    :param mat_distance:  distance matrix,  0 index for hub
    :param cost_uav:  cost coefficient of uav

    :return: list_route:  list of nodes in truck route
    :return: dict_uav_route:  dict of uav routes
    """

    log.info(msg=">>> TSP model, GG formulation, start")

    model = Model('tsp_gg')

    """ variables """

    # if truck flow from i to j
    x = model.addVars([(i, j) for i in range(0, num_node + 1) for j in range(0, num_node + 1)], vtype=GRB.BINARY,
                      name='x_')

    # if uav flow from i to j
    y = model.addVars([(i, j) for i in range(0, num_node + 1) for j in range(0, num_node + 1)], vtype=GRB.BINARY,
                      name='y_')

    # if served by truck
    s = model.addVars([i for i in range(1, num_node + 1)], vtype=GRB.BINARY, name='s_')

    # sub-loop elimination
    a = model.addVars([(i, j) for i in range(0, num_node + 1) for j in range(0, num_node + 1)], vtype=GRB.CONTINUOUS,
                      name='a_')

    """ constraints """

    # cons 1: stream in / out
    for i in range(0, num_node + 1):
        # nodes can only served by truck
        if i not in list_node_uav:
            model.addConstr(quicksum(x[j, i] for j in range(0, num_node + 1)) == 1, name='cons_1_in_x_[{}]'.format(i))
            model.addConstr(quicksum(x[i, j] for j in range(0, num_node + 1)) == 1, name='cons_1_out_x_[{}]'.format(i))

        # nodes can served by truck or uav
        else:
            model.addConstr(quicksum(x[j, i] + y[j, i] for j in range(0, num_node + 1)) == 1,
                            name='cons_1_in_xy_[{}]'.format(i))
            model.addConstr(quicksum(x[i, j] + y[i, j] for j in range(0, num_node + 1)) == 1,
                            name='cons_1_out_xy_[{}]'.format(i))

            # truck or uav
            model.addConstr(quicksum(y[j, i] for j in range(0, num_node + 1))
                            == quicksum(y[i, j] for j in range(0, num_node + 1)), name='cons_1_y[{}]'.format(i))

    # cons 2: self-loop elimination
    model.addConstr(quicksum(x[i, i] for i in range(0, num_node + 1)) == 0, name='cons_2_x')
    model.addConstr(quicksum(y[i, i] for i in range(0, num_node + 1)) == 0, name='cons_2_y')

    # cons 3: get, if served by truck
    for i in range(1, num_node + 1):
        model.addConstr(s[i] == quicksum(x[i, j] for j in range(0, num_node + 1)), name='cons_3_[{}]'.format(i))

    # cons 4: sub-loop elimination
    for i in range(1, num_node + 1):
        list_n_i = list(range(0, i)) + list(range(i + 1, num_node + 1))
        list_n_i_ori = list(range(1, i)) + list(range(i + 1, num_node + 1))
        model.addConstr(quicksum(a[i, j] for j in list_n_i) - quicksum(a[j, i] for j in list_n_i_ori) == s[i],
                        name='cons_4_a_[{}]'.format(i))

        for j in range(0, num_node + 1):
            model.addConstr(a[i, j] <= num_node * x[i, j], name='cons_4_ax_[{},{}]'.format(i, j))

    # cons 5: uav order
    for j in list_node_uav:
        for i in range(0, num_node + 1):
            for k in range(0, num_node + 1):
                model.addConstr(y[i, j] + y[j, k] <= 2 * x[i, k] + 1, name='cons_5_[{},{},{}]'.format(i, j, k))

    # cons 6: only one uav
    for i in range(0, num_node + 1):
        model.addConstr(quicksum(y[i, j] for j in range(0, num_node + 1)) <= 1, name='cons_6_out_[{}]'.format(i))
        model.addConstr(quicksum(y[j, i] for j in range(0, num_node + 1)) <= 1, name='cons_6_in_[{}]'.format(i))

    """ objective & solve """

    # obj: total weighted distance
    cost = quicksum(x[i, j] * mat_distance[i][j] + y[i, j] * mat_distance[i][j] * cost_uav
                    for i in range(0, num_node + 1) for j in range(0, num_node + 1))
    model.setObjective(cost, sense=GRB.MINIMIZE)

    model.setParam(GRB.Param.TimeLimit, 300)
    model.setParam(GRB.Param.MIPGap, 0.02)

    dts_solve = datetime.now()
    model.optimize()
    dte_solve = datetime.now()
    tm_solve = round((dte_solve - dts_solve).seconds + (dte_solve - dts_solve).microseconds / (10 ** 6), 3)
    log.info(msg="solving time:  {} s".format(tm_solve))

    """ result """

    x_ = model.getAttr('X', x)
    y_ = model.getAttr('X', y)
    s_ = model.getAttr('X', s)

    # truck route
    list_route = [0]
    to = None
    while to != 0:
        for i in range(0, num_node + 1):
            if x_[list_route[-1], i] > 0.9:
                to = i
                list_route.append(i)
                break

    # uav routes
    list_node_uav_ = []
    dict_uav_route = {}
    for j in list_node_uav:
        if s_[j] < 0.1:
            list_node_uav_.append(j)

            fr, to = None, None
            for i in range(0, num_node + 1):
                if y_[i, j] > 0.9:
                    fr = i
                    break
            for k in range(0, num_node + 1):
                if y_[j, k] > 0.9:
                    to = k
                    break
            tup_fr_to = (fr, to)
            dict_uav_route[j] = tup_fr_to
    log.info(msg="nodes can be served by uav:  {}".format(list_node_uav))
    log.info(msg="nodes served by uav:  {}".format(list_node_uav_))

    log.info(msg="<<< TSP model, GG formulation, end")

    return list_route, dict_uav_route

VRP:

from datetime import datetime
from typing import List, Dict, Tuple

from gurobipy import *

from utils.logger import log_vrp as log


def vrp_load(num_node: int, list_node_uav: List[int], list_weight: List[int], upper_load: int,
             mat_distance: List[List[int]], cost_uav: float = 0.5) \
        -> Tuple[List[List[int]], Dict[int, Tuple[int, int]]]:
    """
    VRP model, load limit mode

    :param num_node:  number of nodes
    :param list_node_uav:  nodes can be served by uav
    :param list_weight:  weight of each node
    :param upper_load:  upper limit of vehicle loads
    :param mat_distance:  distance matrix,  0 index for hub
    :param cost_uav:  cost coefficient of uav

    :return: list_routes:  list of truck routes
    :return: dict_uav_route:  dict of uav routes
    """

    log.info(msg=">>> VRP model, load limit mode, start")

    model = Model('vrp_load')

    """ variables """

    # if truck flow from i to j
    x = model.addVars([(i, j) for i in range(0, num_node + 1) for j in range(0, num_node + 1)], vtype=GRB.BINARY,
                      name='x_')

    # if uav flow from i to j
    y = model.addVars([(i, j) for i in range(0, num_node + 1) for j in range(0, num_node + 1)], vtype=GRB.BINARY,
                      name='y_')

    # if served by truck
    s = model.addVars([i for i in range(1, num_node + 1)], vtype=GRB.BINARY, name='s_')

    # load limit: arc weight
    z = model.addVars([(i, j) for i in range(0, num_node + 1) for j in range(0, num_node + 1)], ub=upper_load,
                      vtype=GRB.CONTINUOUS, name='a_')

    """ constraints """

    # cons 1: stream in / out, not including origin
    for i in range(1, num_node + 1):
        # nodes can only served by truck
        if i not in list_node_uav:
            model.addConstr(quicksum(x[j, i] for j in range(0, num_node + 1)) == 1, name='cons_1_in_x_[{}]'.format(i))
            model.addConstr(quicksum(x[i, j] for j in range(0, num_node + 1)) == 1, name='cons_1_out_x_[{}]'.format(i))

        # nodes can served by truck or uav
        else:
            model.addConstr(quicksum(x[j, i] + y[j, i] for j in range(0, num_node + 1)) == 1,
                            name='cons_1_in_xy_[{}]'.format(i))
            model.addConstr(quicksum(x[i, j] + y[i, j] for j in range(0, num_node + 1)) == 1,
                            name='cons_1_out_xy_[{}]'.format(i))

            # truck or uav
            model.addConstr(quicksum(y[j, i] for j in range(0, num_node + 1))
                            == quicksum(y[i, j] for j in range(0, num_node + 1)), name='cons_1_y[{}]'.format(i))

    # cons 2: self-loop elimination
    model.addConstr(quicksum(x[i, i] for i in range(0, num_node + 1)) == 0, name='cons_2_x')
    model.addConstr(quicksum(y[i, i] for i in range(0, num_node + 1)) == 0, name='cons_2_y')

    # cons 3: get, if served by truck
    for i in range(1, num_node + 1):
        model.addConstr(s[i] == quicksum(x[i, j] for j in range(0, num_node + 1)), name='cons_3_[{}]'.format(i))

    # cons 4: load limit, including sub-loop elimination
    big_m = 10 ** 6
    for j in range(1, num_node + 1):
        for i in range(0, num_node + 1):
            list_not_j = list(range(0, j)) + list(range(j + 1, num_node + 1))
            list_uav_not_j = list_node_uav.copy()
            if j in list_uav_not_j:
                list_uav_not_j.remove(j)
            model.addConstr(z[i, j] >= list_weight[j - 1] + quicksum(z[j, k] for k in list_not_j)
                            + quicksum(y[j, u] * list_weight[u - 1] for u in list_uav_not_j) + (x[i, j] - 1) * big_m,
                            name='cons_4_[{},{}]'.format(i, j))

            model.addConstr(z[i, j] <= x[i, j] * big_m, name='cons_4_zx_[{},{}]'.format(i, j))

            # todo: sun-loop elimination:  2-node loop
            if i:
                model.addConstr(x[i, j] + x[j, i] <= 1, name='cons_4_loop_2[{},{}]'.format(i, j))

    # cons 5: uav order
    for j in list_node_uav:
        for i in range(0, num_node + 1):
            for k in range(0, num_node + 1):
                model.addConstr(y[i, j] + y[j, k] <= 2 * x[i, k] + 1, name='cons_5_[{},{},{}]'.format(i, j, k))

    # cons 6: only one uav
    for i in range(0, num_node + 1):
        model.addConstr(quicksum(y[i, j] for j in range(0, num_node + 1)) <= 1, name='cons_6_out_[{}]'.format(i))
        model.addConstr(quicksum(y[j, i] for j in range(0, num_node + 1)) <= 1, name='cons_6_in_[{}]'.format(i))

    # cons 7: valid inequality
    model.addConstr(quicksum(x[0, j] for j in range(1, num_node + 1)) >= math.ceil(sum(list_weight) / upper_load),
                    name='cons_7')

    """ objective & solve """

    num_vehicle = quicksum(x[0, j] for j in range(1, num_node + 1))
    sum_distance = quicksum(x[i, j] * mat_distance[i][j] + y[i, j] * mat_distance[i][j] * cost_uav
                            for i in range(0, num_node + 1) for j in range(0, num_node + 1))
    model.setObjective(num_vehicle * big_m + sum_distance, sense=GRB.MINIMIZE)

    model.setParam(GRB.Param.TimeLimit, 600)
    model.setParam(GRB.Param.MIPGap, 0.05)

    dts_solve = datetime.now()
    model.optimize()
    dte_solve = datetime.now()
    tm_solve = round((dte_solve - dts_solve).seconds + (dte_solve - dts_solve).microseconds / (10 ** 6), 3)
    log.info(msg="solving time:  {} s".format(tm_solve))

    """ result """

    x_ = model.getAttr('X', x)
    y_ = model.getAttr('X', y)
    s_ = model.getAttr('X', s)

    num_vehicle_ = int(sum(x_[0, j] for j in range(1, num_node + 1)))
    log.info("total vehicles: {}".format(num_vehicle_))

    # uav routes
    list_node_uav_ = []
    dict_uav_route = {}
    for j in list_node_uav:
        if s_[j] < 0.1:
            list_node_uav_.append(j)

            fr, to = None, None
            for i in range(0, num_node + 1):
                if y_[i, j] > 0.9:
                    fr = i
                    break
            for k in range(0, num_node + 1):
                if y_[j, k] > 0.9:
                    to = k
                    break
            tup_fr_to = (fr, to)
            dict_uav_route[j] = tup_fr_to
    log.info(msg="nodes can be served by uav:  {}".format(list_node_uav))
    log.info(msg="nodes served by uav:  {}".format(list_node_uav_))

    # truck routes
    list_routes = []
    set_node = set(range(0, num_node + 1))
    while len(set_node) > 1 + len(list_node_uav_):  # todo: number of nodes served by truck
        list_route_cur = [0]
        to = None
        while to != 0:
            if to:
                set_node.remove(to)
            for i in set_node:
                if x_[list_route_cur[-1], i] > 0.9:
                    to = i
                    list_route_cur.append(to)
                    break
        list_routes.append(list_route_cur)

    log.info(msg="<<< TSP model, GG formulation, end")

    return list_routes, dict_uav_route

工具模块(日志打印工具)


源码中采用调用 logging 打印日志的方法。
参见本人另一篇博客:https://blog.csdn.net/Zhang_0702_China/article/details/107982958

import os
import logging
from logging.handlers import TimedRotatingFileHandler


PATH = os.path.dirname(os.path.dirname(__file__))


class Logger(object):
    """
    log writer
    """

    def __init__(self, log_name: str):
        """
        log writer, initialise

        :param log_name:  file name of log
        """

        self.log_name = log_name
        self.log = logging.getLogger(name=self.log_name)
        self.log.setLevel(logging.INFO)

        # initialise logging
        logging.basicConfig()

        # output format
        fmt_st = "%(asctime)s[%(levelname)s][%(processName)s][%(threadName)s]:%(message)s"
        formatter = logging.Formatter(fmt=fmt_st)

        # log file
        path_root = os.path.join(PATH, "log")  # root directory of log
        if not os.path.exists(path_root):
            os.makedirs(name=path_root)
        path = os.path.join(path_root, self.log_name)

        # level and format
        file_handler = logging.handlers.TimedRotatingFileHandler(filename=path)
        file_handler.setLevel(level=logging.INFO)
        file_handler.setFormatter(fmt=formatter)
        self.log.addHandler(hdlr=file_handler)

    def info(self, msg: str):
        """
        info

        :param msg:  info

        :return: nothing
        """

        msg_ = " " + msg
        self.log.info(msg=msg_)

    def error(self, msg: str):
        """
        error info

        :param msg:  error info

        :return: nothing
        """

        msg_ = " " + msg
        self.log.error(msg=msg_)


log_tsp = Logger(log_name='tsp.log')
log_vrp = Logger(log_name='vrp.log')

可视化模块


TSP:

import os
from typing import List, Dict, Tuple
import matplotlib.pyplot as plt

from config import tsp as config


PATH = os.path.dirname(os.path.dirname(__file__))


def scatter_route_tsp(list_route: List[int], dict_uav_route: Dict[int, Tuple[int, int]]):
    """
    scatters and route of tsp result

    :param list_route:  list of nodes in truck route
    :param dict_uav_route:  dict of uav routes

    :return: nothing
    """

    num_node = config.NUM_NODE
    list_node_uav = config.LIST_NODE_UAV
    origin = config.ORIGIN
    list_coordinate_ = config.LIST_COORDINATE_
    range_coordinate = config.RANGE_COORDINATE

    # scatters
    plt.scatter(x=origin[0], y=origin[1], s=50, c='black', label='origin')
    for i in range(1, num_node + 1):
        color = 'blue' if i not in list_node_uav else 'red'
        plt.scatter(x=list_coordinate_[i][0], y=list_coordinate_[i][1], s=20, c=color, label=str(i))

    width = 0.2
    head_width = 2
    head_length = 3
    alpha = 0.7
    color_truck, color_uav = 'green', 'orange'
    line_width = 0.5

    # truck route
    for i in range(len(list_route) - 1):
        plt.arrow(x=list_coordinate_[list_route[i]][0], y=list_coordinate_[list_route[i]][1],
                  dx=list_coordinate_[list_route[i + 1]][0] - list_coordinate_[list_route[i]][0],
                  dy=list_coordinate_[list_route[i + 1]][1] - list_coordinate_[list_route[i]][1],
                  width=width, length_includes_head=True, head_width=head_width, head_length=head_length,
                  alpha=alpha, color=color_truck, linestyle='-', linewidth=line_width)

    # uav route
    for i in dict_uav_route.keys():
        plt.arrow(x=list_coordinate_[dict_uav_route[i][0]][0], y=list_coordinate_[dict_uav_route[i][0]][1],
                  dx=list_coordinate_[i][0] - list_coordinate_[dict_uav_route[i][0]][0],
                  dy=list_coordinate_[i][1] - list_coordinate_[dict_uav_route[i][0]][1],
                  width=width, length_includes_head=True, head_width=head_width, head_length=head_length,
                  alpha=alpha, color=color_uav, linestyle='--', linewidth=line_width)
        plt.arrow(x=list_coordinate_[i][0], y=list_coordinate_[i][1],
                  dx=list_coordinate_[dict_uav_route[i][1]][0] - list_coordinate_[i][0],
                  dy=list_coordinate_[dict_uav_route[i][1]][1] - list_coordinate_[i][1],
                  width=width, length_includes_head=True, head_width=head_width, head_length=head_length,
                  alpha=alpha, color=color_uav, linestyle='--', linewidth=line_width)

    # x, y range
    edge = 5
    plt.xlim(range_coordinate[0] - edge, range_coordinate[1] + edge)
    plt.ylim(range_coordinate[0] - edge, range_coordinate[1] + edge)

    # save
    path = os.path.join(PATH, "output/tsp.png")
    plt.savefig(path)

VRP:

import os
from typing import List, Dict, Tuple
import matplotlib.pyplot as plt

from config import vrp as config


PATH = os.path.dirname(os.path.dirname(__file__))


def scatter_route_vrp(list_routes: List[List[int]], dict_uav_route: Dict[int, Tuple[int, int]]):
    """
    scatters and route of vrp result

    :param list_routes:  list of truck route
    :param dict_uav_route:  dict of uav routes

    :return: nothing
    """

    num_node = config.NUM_NODE
    list_node_uav = config.LIST_NODE_UAV
    origin = config.ORIGIN
    list_coordinate_ = config.LIST_COORDINATE_
    range_coordinate = config.RANGE_COORDINATE

    # scatters
    plt.scatter(x=origin[0], y=origin[1], s=50, c='black', label='origin')
    for i in range(1, num_node + 1):
        color = 'blue' if i not in list_node_uav else 'red'
        plt.scatter(x=list_coordinate_[i][0], y=list_coordinate_[i][1], s=20, c=color, label=str(i))

    width = 0.2
    head_width = 2
    head_length = 3
    alpha = 0.7
    color_truck, color_uav = 'green', 'orange'
    line_width = 0.5

    # truck routes
    for route in list_routes:
        for i in range(len(route) - 1):
            plt.arrow(x=list_coordinate_[route[i]][0], y=list_coordinate_[route[i]][1],
                      dx=list_coordinate_[route[i + 1]][0] - list_coordinate_[route[i]][0],
                      dy=list_coordinate_[route[i + 1]][1] - list_coordinate_[route[i]][1],
                      width=width, length_includes_head=True, head_width=head_width, head_length=head_length,
                      alpha=alpha, color=color_truck, linestyle='-', linewidth=line_width)

    # uav routes
    for i in dict_uav_route.keys():
        plt.arrow(x=list_coordinate_[dict_uav_route[i][0]][0], y=list_coordinate_[dict_uav_route[i][0]][1],
                  dx=list_coordinate_[i][0] - list_coordinate_[dict_uav_route[i][0]][0],
                  dy=list_coordinate_[i][1] - list_coordinate_[dict_uav_route[i][0]][1],
                  width=width, length_includes_head=True, head_width=head_width, head_length=head_length,
                  alpha=alpha, color=color_uav, linestyle='--', linewidth=line_width)
        plt.arrow(x=list_coordinate_[i][0], y=list_coordinate_[i][1],
                  dx=list_coordinate_[dict_uav_route[i][1]][0] - list_coordinate_[i][0],
                  dy=list_coordinate_[dict_uav_route[i][1]][1] - list_coordinate_[i][1],
                  width=width, length_includes_head=True, head_width=head_width, head_length=head_length,
                  alpha=alpha, color=color_uav, linestyle='--', linewidth=line_width)

    # x, y range
    edge = 5
    plt.xlim(range_coordinate[0] - edge, range_coordinate[1] + edge)
    plt.ylim(range_coordinate[0] - edge, range_coordinate[1] + edge)

    # save
    path = os.path.join(PATH, "output/vrp.png")
    plt.savefig(path)

主程序


TSP(main_tsp.py):

from datetime import datetime

from utils.logger import log_tsp as log
from config import tsp as config
from model.tsp import tsp_gg
from visual.tsp import scatter_route_tsp


DTS = datetime.now()

NUM_NODE = config.NUM_NODE
LIST_NODE_UAV = config.LIST_NODE_UAV.copy()
MAT_DISTANCE = config.MAT_DISTANCE.copy()
COST_UAV = config.COST_UAV
log.info(msg="number of nodes:  {}".format(NUM_NODE))
log.info(msg="node can served by uav:  {}".format(LIST_NODE_UAV))


# model
list_route, dict_uav_route = tsp_gg(num_node=NUM_NODE, list_node_uav=LIST_NODE_UAV, mat_distance=MAT_DISTANCE,
                                    cost_uav=COST_UAV)
log.info(msg="truck route:  {}".format(list_route))
for i in dict_uav_route.keys():
    log.info(msg="uav served node:  {},  {}".format(i, dict_uav_route[i]))

# visualisation
scatter_route_tsp(list_route=list_route, dict_uav_route=dict_uav_route)


DTE = datetime.now()
TM = round((DTE - DTS).seconds + (DTE - DTS).microseconds / (10 ** 6), 3)
log.info(msg="total time:  {} s".format(TM))

VRP(main_vrp.py):

from datetime import datetime

from utils.logger import log_vrp as log
from config import vrp as config
from model.vrp import vrp_load
from visual.vrp import scatter_route_vrp


DTS = datetime.now()

NUM_NODE = config.NUM_NODE
LIST_NODE_UAV = config.LIST_NODE_UAV.copy()
LIST_WEIGHT = config.LIST_WEIGHT
UPPER_LOAD = config.UPPER_LOAD
MAT_DISTANCE = config.MAT_DISTANCE.copy()
COST_UAV = config.COST_UAV
log.info(msg="number of nodes:  {}".format(NUM_NODE))
log.info(msg="node can served by uav:  {}".format(LIST_NODE_UAV))


# model
list_routes, dict_uav_route = vrp_load(
    num_node=NUM_NODE, list_node_uav=LIST_NODE_UAV, list_weight=LIST_WEIGHT, upper_load=UPPER_LOAD,
    mat_distance=MAT_DISTANCE, cost_uav=COST_UAV)
for i in range(len(list_routes)):
    log.info(msg="truck route {}:  {}".format(i + 1, list_routes[i]))
for i in dict_uav_route.keys():
    log.info(msg="uav served node:  {},  {}".format(i, dict_uav_route[i]))

# visualisation
scatter_route_vrp(list_routes=list_routes, dict_uav_route=dict_uav_route)


DTE = datetime.now()
TM = round((DTE - DTS).seconds + (DTE - DTS).microseconds / (10 ** 6), 3)
log.info(msg="total time:  {} s".format(TM))

运行效果


TSP


如前面参数与输入数据模块所示,源码中的 TSP 算例有 50 个点,其中后面 20 个可使用无人机配送,无人机的距离成本系数设为 0.2(即同样的距离又无人机配送的成本为卡车配送的 20%),源码的运行日志如下:

2021-08-04 19:37:03,923[INFO][MainProcess][MainThread]: random data generating time:  0.004 s
2021-08-04 19:37:04,787[INFO][MainProcess][MainThread]: number of nodes:  50
2021-08-04 19:37:04,787[INFO][MainProcess][MainThread]: node can served by uav:  [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2021-08-04 19:37:04,787[INFO][MainProcess][MainThread]: >>> TSP model, GG formulation, start
2021-08-04 19:38:16,624[INFO][MainProcess][MainThread]: solving time:  49.773 s
2021-08-04 19:38:16,633[INFO][MainProcess][MainThread]: nodes can be served by uav:  [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2021-08-04 19:38:16,633[INFO][MainProcess][MainThread]: nodes served by uav:  [31, 32, 33, 36, 37, 39, 40, 41, 42, 44, 49, 50]
2021-08-04 19:38:16,634[INFO][MainProcess][MainThread]: <<< TSP model, GG formulation, end
2021-08-04 19:38:16,690[INFO][MainProcess][MainThread]: truck route:  [0, 34, 24, 27, 5, 1, 21, 14, 35, 48, 12, 43, 17, 20, 18, 19, 2, 26, 15, 8, 47, 13, 29, 38, 28, 9, 22, 6, 4, 30, 10, 11, 25, 46, 16, 3, 23, 45, 7, 0]
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  31,  (20, 18)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  32,  (10, 11)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  33,  (22, 6)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  36,  (1, 21)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  37,  (13, 29)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  39,  (9, 22)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  40,  (17, 20)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  41,  (18, 19)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  42,  (26, 15)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  44,  (21, 14)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  49,  (19, 2)
2021-08-04 19:38:16,691[INFO][MainProcess][MainThread]: uav served node:  50,  (6, 4)
2021-08-04 19:38:17,174[INFO][MainProcess][MainThread]: total time:  72.387 s

可视化结果图:
<代码分享> 一种无人机配合卡车配送的车辆路径规划模型

VRP


关于 VRP 模型的算例,源码中的设置为 50 个点,其中最后 10 个点可使用无人机配送,无人机的距离成本系数设为 0.1,每辆车的载重上限为 100,每个物品的重量上限为 20,源码的运行日志如下:

2021-08-04 19:40:21,300[INFO][MainProcess][MainThread]: total weight:  486
2021-08-04 19:40:21,300[INFO][MainProcess][MainThread]: random data generating time:  0.004 s
2021-08-04 19:40:22,168[INFO][MainProcess][MainThread]: number of nodes:  50
2021-08-04 19:40:22,168[INFO][MainProcess][MainThread]: node can served by uav:  [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2021-08-04 19:40:22,168[INFO][MainProcess][MainThread]: >>> VRP model, load limit mode, start
2021-08-04 19:41:23,075[INFO][MainProcess][MainThread]: solving time:  38.906 s
2021-08-04 19:41:23,092[INFO][MainProcess][MainThread]: total vehicles: 5
2021-08-04 19:41:23,093[INFO][MainProcess][MainThread]: nodes can be served by uav:  [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2021-08-04 19:41:23,094[INFO][MainProcess][MainThread]: nodes served by uav:  [42, 43, 44, 45, 46, 47, 48, 49, 50]
2021-08-04 19:41:23,094[INFO][MainProcess][MainThread]: <<< TSP model, GG formulation, end
2021-08-04 19:41:23,175[INFO][MainProcess][MainThread]: truck route 1:  [0, 6, 4, 30, 25, 11, 10, 32, 3, 16, 23, 0]
2021-08-04 19:41:23,175[INFO][MainProcess][MainThread]: truck route 2:  [0, 7, 33, 22, 39, 9, 13, 29, 37, 38, 28, 0]
2021-08-04 19:41:23,175[INFO][MainProcess][MainThread]: truck route 3:  [0, 19, 2, 26, 15, 8, 0]
2021-08-04 19:41:23,175[INFO][MainProcess][MainThread]: truck route 4:  [0, 34, 24, 36, 1, 5, 27, 0]
2021-08-04 19:41:23,175[INFO][MainProcess][MainThread]: truck route 5:  [0, 41, 31, 18, 20, 17, 40, 12, 35, 14, 21, 0]
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  42,  (26, 15)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  43,  (19, 2)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  44,  (36, 1)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  45,  (34, 24)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  46,  (3, 16)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  47,  (15, 8)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  48,  (35, 14)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  49,  (2, 26)
2021-08-04 19:41:23,176[INFO][MainProcess][MainThread]: uav served node:  50,  (0, 6)
2021-08-04 19:41:23,652[INFO][MainProcess][MainThread]: total time:  61.484 s

可视化结果图:
<代码分享> 一种无人机配合卡车配送的车辆路径规划模型


上一篇:A* 路径搜索算法介绍及完整代码


下一篇:win10路由