2.1. Gurobi建模介绍

2.1. Gurobi建模介绍

本部分可以借鉴Gurobi的手册来讲述。

模型类

模型的Attribute

创建模型

决策变量

决策变量类型

加决策变量

决策变量的Attribute

线性表达式

线性表达式

非线性表达式

约束

添加线性约束

添加非线性约束

加General约束

添加Range约束

约束的Attribute

目标函数

添加目标函数

目标函数的Attribute

求解模型

求解模型

求解模型参数设置

输出解的结果

参数设置和调优

2.2 Gurobi的简单应用案例

简单LP

Assignment Problem

按行建模

按列建模

文件读入

2.3 Gurobi的高级应用

Callback的工作逻辑使用

Python+Gurobi: 用callback实现TSP的subtour-elimination

首先,我们还是以VRP上古大神solomon [^1]的benchmark为算例,来进行今天代码的数值实验部分。

算例下载地址:https://www.sintef.no/projectweb/top/vrptw/solomon-benchmark/100-customers/

在这之前,我想先说一下GurobiCPLEX里面的callback是怎么个逻辑:

callback的工作逻辑: 王者荣耀版独家解读

下面我夹杂王者荣耀的角度来轻松解释callback是怎么起作用的,打农药的小伙伴应该秒懂。(有些地方不严谨,但是大概是这么个意思,这段本来就是辅助理解,不严谨的地方可以私信我,我再修改)

1.之前说过,subtour-elimination的想法,是想把所有的子集列举出来,为每一个子集添加破圈约束,但是这么做太慢。
2.于是我们想,先不加subtour-elimination的约束,我们先把只含有前两组约束的IP输入给Gurobi求解,Gurobi当然会先把IP的整数约束松弛掉,把模型变成LP,然后调用branch and bound算法,并将IP松弛后的relaxed LP作为根节点,进行branch and bound tree的迭代。
下面高能解释来了 我们把这个算法的迭代比作一场王者双排排位赛。假设我们准备开始玩游戏,我方打野选了野王Gurobi,OK,我见势立马一手奶妈蔡文姬,死跟打野,只干两件事1. 探视野(识别subtour)和2. 给助攻(根据subtour构建破圈约束)。Gurobi大佬构建模型,并且加入了前两组约束。(铭文带的不够呀,我有点慌,大佬却说,躺好看我carry, 帮我看蓝探视野),而我们也不示弱, 选了subtour-elimination的辅助装出装策略。(也就是subtour-eliminationcallback函数,用于添加通过callback的方式添加subtour-elimination约束的)
3.游戏开始,Gurobi打着前两组约束,并且设置model.Params.lazyConstraints = 1也就是给我(软辅蔡文姬)发出跟着我的信号。然后拉着我一起双排开始了游戏,代码中就是model.optimize(subtourelim)
OK,算法开始迭代。野王Gurobi还是基本操作,熟练的branch and bound在野区以最适合的刷野路径刷野。在刷野(迭代)的过程中,在每个branch and bound tree的结点处,Gurobi会去调用各种变式的simplex算法得到该节点的解。如果得到了一个整数解(也可以是得到小数解就操作),我们可以在这个地方,人为的插一脚,相当于Gurobi老哥正在屁颠屁颠刷野(跑算法)呢,我们在旁边探视野(做监工),一直就这么直勾的看着。我们看到老哥得到了一个整数解(或者一个小数可行解),一机灵,激动地过去拍拍Gurobi老哥的肩膀说,大佬,我拿一下这个节点的LP解哈,您继续
4.OK, 拿到了LP的解以后,我们自己来看看这个解中存不存在subtour,如果我们检测完后发现不存在。尴尬,我们假装啥也没发生。继续静静地看着Gurobi老哥刷野(算法迭代)。下一次,Gurobi老哥又得到了一个整数解(或者一个小数可行解),我们再厚脸皮去拿过来检测,结果发现有2-5-8-2这个子环路混子混在里面,这次可被我逮个正着哈,小伙儿,你子环路了幺。我们激动地大声告诉Gurobi老哥:“老哥,老哥,子环路2-5-8-2在这儿挑衅你,你去GUNK它,把2-5-8-2这个混子送回家”,顺便我们还根据这个子环路2-5-8-2的特点,快速给Gurobi老哥想出了一套连招:老哥,你1433223就可以秒他!!! 哈哈,在实际中,把这个子环路踢出去的方法(也就是刚刚的连招)就是加下面这个约束: x 25 + x 58 + x 82 ⩽ 2 x_{25}+x_{58}+x_{82} \leqslant 2 x25+x58+x822这里还有个坑,虽然你也可以写成等价的 x 25 + x 58 + x 82 < 3 x_{25}+x_{58}+x_{82} < 3 x25+x58+x82<3,但是求解器是不接受 < < <, > > >这样的约束的,你要硬加,那就报错。很多人其实并不知道这一点,我在这里提一下。
5.接上面,Gurobi老哥非常强,手脚麻利动作快,脑子很好使能同时处理多个信息,听到了我们奶妈蔡文姬报视野前面草丛有个2-5-8-2的ADC很浪,落单了,还1433223连招可以秒他,回头敬个礼说:好嘞,知道了,放心吧,瞧好了您呐,看我把他怼出去哈。看这老哥如此稳的操作,我心中的默默点赞:同九义,何汝秀。然后继续监工。之后我就再也没见过2-5-8-2这个子环路混子。之后的监工中,我这奶妈蔡文姬又陆续把1-5-8-13-4-7-9-3等一众混子报给Gurobi老哥,老哥一一将他们1433223送回家。
6.由于我方打野Gurobi老哥刚开始只加入了前两组约束,轻车简从,走路带风,身为奶妈蔡文姬的我紧跟打野,时刻监视打野行为,并不断为打野探视野,找敌方落单ADC,并每次都及时给个助攻subtour-elimination constraints x 25 + x 58 + x 82 ⩽ 2 x_{25}+x_{58}+x_{82} \leqslant 2 x25+x58+x822),抢个人头,最终Gurobi老哥轻松carry全场,拿到2-5-8-23-4-7-9-31-5-8-1等3个人头 。直击敌方水晶,获得最优解[0, 4, 3, 7, 1, 2, 5, 8, 9, 6, 0]。评分127,夺得胜方MVP。起立,鼓掌!!!是的,每次都是这样。

上面的描述并不完美,但是我想,应该能给你一些辅助理解callback工作逻辑的帮助。

使用callback的通用步骤

其实总结一下,使用callback的方法分为下面几步(只针对本问题)

  1. 第一步:利用Gurobi构建数学模型,只加入前两组约束;
  2. 第二步:构建一个用来识别subtour并返回消除子环路约束的函数subtourelim(model, where)(注意,这个函数的参数model, where是固定的,求解器规定的)。这个函数用于:拿到整数规划分支定界迭代过程中当前结点的解的信息,并根据当前节点的解,识别子环路,如有则返回消除子环路的约束,否则不作操作。
  3. 第三步:设置使用lazyConstraints,并启动优化算法求解模型,也就是model.optimize(subtourelim),而且必须以callback函数subtourelim(model, where)为参数,具体代码为:
model._vars = X 
model.Params.lazyConstraints = 1
model.optimize(subtourelim)

【Remark】这里,subtourelim(model, where)中,添加子环路消除约束是以lazyConstraints的形式添加的,lazyConstraints就是不在建模一开始就加入,而是在算法迭代的过程中,动态的在branch and bound的分支节点处才加入的约束。可以通过callback函数,控制在节点的解满足什么样的条件下,我们去构建特定形式的约束,这个约束以lazyConstraints的形式构建并添加到求解的函数model.optimize()中,然后Gurobi就可以自动的识别,且调用callback函数,按照你的要求在求解过程中把约束加进去。这一招branch and cut, benders decomposition, row generation的时候用的非常多。想要进阶的小伙伴这招儿还是必须要攻克的。

Callback的使用:callback实现subtour-elimination的详细代码

这部分代码有点长,待我明天再放出来把。算了,还是直接放上来把。

首先定义一些读取数据的函数:

  • readData(path, nodeNum):读取.txt文档中的算例数据;
  • reportMIP(model, Routes):获得并打印最优解信息;
  • getValue(var_dict, nodeNum):获得决策变量的值,并存储返回一个np.array()数组;
  • getRoute(x_value):根据解x_value得到该解对应的路径。
# _*_coding:utf-8 _*_
'''
@author: Hsinglu Liu
@version: 1.0
@Date: 2019.5.5
'''

from __future__ import print_function
from __future__ import division, print_function
from gurobipy import *
import re;
import math;
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import copy
from matplotlib.lines import lineStyles
import time

starttime = time.time()

# function to read data from .txt files   
def readData(path, nodeNum):
    nodeNum = nodeNum;
    cor_X = []
    cor_Y = []
    
    f = open(path, 'r');
    lines = f.readlines();
    count = 0;
    # read the info
    for line in lines:
        count = count + 1;
        if(count >= 10 and count <= 10 + nodeNum):
            line = line[:-1]
            str = re.split(r" +", line)
            cor_X.append(float(str[2]))
            cor_Y.append(float(str[3]))
                
    # compute the distance matrix
    disMatrix = [([0] * nodeNum) for p in range(nodeNum)]; # 初始化距离矩阵的维度,防止浅拷贝
    # data.disMatrix = [[0] * nodeNum] * nodeNum]; 这个是浅拷贝,容易重复
    for i in range(0, nodeNum):
        for j in range(0, nodeNum):
            temp = (cor_X[i] - cor_X[j])**2 + (cor_Y[i] - cor_Y[j])**2;
            disMatrix[i][j] = (int)(math.sqrt(temp));
#             disMatrix[i][j] = 0.1 * (int)(10 * math.sqrt(temp));
#             if(i == j):
#                 data.disMatrix[i][j] = 0;
#             print("%6.0f" % (math.sqrt(temp)), end = " ");
            temp = 0;
    
    return disMatrix;

def printData(disMatrix):
    print("-------cost matrix-------\n");
    for i in range(len(disMatrix)):
        for j in range(len(disMatrix)):
            #print("%d   %d" % (i, j));
            print("%6.1f" % (disMatrix[i][j]), end = " ");
#             print(disMatrix[i][j], end = " ");
        print();
        
def reportMIP(model, Routes):
    if model.status == GRB.OPTIMAL:
        print("Best MIP Solution: ", model.objVal, "\n")
        var = model.getVars()
        for i in range(model.numVars):
            if(var[i].x > 0):
                print(var[i].varName, " = ", var[i].x)
                print("Optimal route:", Routes[i])
                        
def getValue(var_dict, nodeNum): 
    x_value = np.zeros([nodeNum + 1, nodeNum + 1]) 
    for key in var_dict.keys():   
        a = key[0]
        b = key[1]
        x_value[a][b] = var_dict[key].x  
            
    return x_value    

def getRoute(x_value):
    # 假如是5个点的算例,我们的路径会是1-4-2-3-5-6这样的,因为我们加入了一个虚拟点
    # 也就是当路径长度为6的时候,我们就停止,这个长度和x_value的长度相同
    x = copy.deepcopy(x_value)
#     route_temp.append(0)
    previousPoint = 0
    arcs = []
    route_temp = [previousPoint] 
    count = 0 
    while(len(route_temp) < len(x) and count < len(x)): 
        print('previousPoint: ', previousPoint, 'count: ', count)
        if(x[previousPoint][count] > 0): 
            previousPoint = count  
            route_temp.append(previousPoint) 
            count = 0 
            continue
        else:
            count += 1
    return route_temp         

# cost = [[0, 7, 2, 1, 5], 
#         [7, 0, 3, 6, 8],
#         [2, 3, 0, 4, 2],
#         [1, 6, 4, 0, 9],
#         [5, 8, 2, 9, 0]]

然后定义几个非常关键的用于添加subtour-elimination约束的函数:

  • subtourelim(model, where): callback函数,用于为model对象动态添加subtour-elimination约束;
  • computeDegree(graph): 给定一个graph(二维数组形式),也就是给定一个邻接矩阵,计算出每个结点的degree.(degree=每个结点被进入次数+被离开的次数);
  • findEdges(graph): 给定一个graph(二维数组形式),也就是给定一个邻接矩阵,找到该图中所有的,例如[(1, 2), (2, 4), (2, 5)];
  • subtour(graph):给定一个graph(二维数组形式),也就是给定一个邻接矩阵,找到该图中包含结点数目最少的子环路,例如[2, 3, 5]。

其中,函数subtourelim(model, where)中,调用了函数computeDegree(graph)findEdges(graph)subtour(graph)

# Callback - use lazy constraints to eliminate sub-tours

# Callback - use lazy constraints to eliminate sub-tours

def subtourelim(model, where): 
    if(where == GRB.Callback.MIPSOL): 
        # make a list of edges selected in the solution
        print('model._vars', model._vars)
#         vals = model.cbGetSolution(model._vars)
        x_value = np.zeros([nodeNum + 1, nodeNum + 1]) 
        for m in model.getVars():
            if(m.varName.startswith('x')):
#                 print(var[i].varName)
#                 print(var[i].varName.split('_'))
                a = (int)(m.varName.split('_')[1])  
                b = (int)(m.varName.split('_')[2])
                x_value[a][b] = model.cbGetSolution(m) 
        print("solution = ", x_value)
#         print('key = ', model._vars.keys())
#         selected = []
#         for i in range(nodeNum):
#             for j in range(nodeNum):
#                 if(i != j and x_value[i][j] > 0.5):
#                     selected.append((i, j))
#         selected = tuplelist(selected)
# #         selected = tuplelist((i,j) for i in range(nodeNum), for if x_value[i][j] > 0.5)
#         print('selected = ', selected)
        # find the shortest cycle in the selected edge list
        tour = subtour(x_value)
        print('tour = ', tour) 
        if(len(tour) < nodeNum + 1):  
            # add subtour elimination constraint for every pair of cities in tour
            print("---add sub tour elimination constraint--")
#             model.cbLazy(quicksum(model._vars[i][j]
#                                       for i in tour
#                                       for j in tour
#                                       if i != j)
#                              <= len(tour)-1)
#             LinExpr = quicksum(model._vars[i][j]
#                                       for i in tour
#                                       for j in tour
#                                       if i != j)
            for i,j in itertools.combinations(tour, 2):
                print(i,j) 
    
            model.cbLazy(quicksum(model._vars[i, j]
                                      for i,j in itertools.combinations(tour, 2))
                             <= len(tour)-1)
            LinExpr = quicksum(model._vars[i, j]
                                      for i,j in itertools.combinations(tour, 2))
            print('LinExpr = ', LinExpr)
            print('RHS = ', len(tour)-1)  

# compute the degree of each node in given graph 
def computeDegree(graph):
    degree = np.zeros(len(graph))
    for i in range(len(graph)):
        for j in range(len(graph)):
            if(graph[i][j] > 0.5):
                degree[i] = degree[i] + 1
                degree[j] = degree[j] + 1
    print('degree', degree)
    return degree 

# given a graph, get the edges of this graph  
def findEdges(graph):
    edges = []
    for i in range(1, len(graph)):
        for j in range(1, len(graph)):
            if(graph[i][j] > 0.5):
                edges.append((i, j))
    
    return edges 



# Given a tuplelist of edges, find the shortest subtour
def subtour(graph):
    # compute degree of each node
    degree = computeDegree(graph)
    unvisited = []
    for i in range(1, len(degree)):
        if(degree[i] >= 2):
            unvisited.append(i)
    cycle = range(0, nodeNum + 1) # initial length has 1 more city
    
    edges = findEdges(graph)
    edges = tuplelist(edges)
    print(edges)
    while unvisited: # true if list is non-empty
        thiscycle = []
        neighbors = unvisited
        while neighbors:  # true if neighbors is non-empty
            current = neighbors[0]
            thiscycle.append(current)
            unvisited.remove(current)
            neighbors = [j for i,j in edges.select(current,'*') if j in unvisited]
            neighbors2 = [i for i,j in edges.select('*',current) if i in unvisited]
            if(neighbors2):
                neighbors.extend(neighbors2)
#             print('current:', current, '\n neighbors', neighbors)
        
        isLink = ((thiscycle[0], thiscycle[-1]) in edges) or ((thiscycle[-1], thiscycle[0]) in edges)
        if(len(cycle) > len(thiscycle) and len(thiscycle) >= 3 and isLink):
#             print('in = ', ((thiscycle[0], thiscycle[-1]) in edges) or ((thiscycle[-1], thiscycle[0]) in edges))
            cycle = thiscycle
            return cycle
    return cycle

然后是建模部分的代码,建模部分相比学运筹的人比较熟悉,这里比较特殊的就是求解时候的几行代码:

  • model.Params.lazyConstraints = 1 : set lazy constraints Parameter
  • model.optimize(subtourelim) : use callback function when executing branch and bound algorithm

猜你喜欢

转载自blog.csdn.net/HsinglukLiu/article/details/107885364
2.1