一篇文章弄懂GUROBI基础操作
Gurobi特有的数据结构
在gurobi里面经常会和tuplelist和tupledict这两种数据结构打交道,理解他们是非常重要的。实际上tuplelist和tupledict分别继承于python的序列数据结构list和dict,所以list和dict的特点它俩都有,list和dict能做的操作tuplelist和tupledict都能做。那tuplelist和tupledict存在的意义是什么?In which situation they are better than their father?理解清楚这一点,tuplelist和tupledict就算是大致搞清楚了——在建模的时候,我们常常会有需要筛选出满足特定条件的下标,或者对满足特定条件的下标做运算。比如
如果用普通字典存放变量quicksum(x[i,j] for j in range(1,3))
但如果变量是用tupledict存储的,那么我们可以直接像下面这么写。
如果用tupledictx.sum("*",[1,2])
是不是一下子就简洁许多?这就是tuplelist和tupledict这两个数据结构设计的核心逻辑,总结一下,你只需要知道tuplelist和tupledict可以让我们快速实现筛选、求和等操作。具体而言,这两个数据结构在他们的爸爸list和dict的基础上额外实现了下面这些方法。
- select(pattern):对于tuplelist,返回满足pattern的所有元组构成的tuplelist,对于tupledict,返回满足pattern的所有变量及其对应索引构成的tupledict。置于这个pattern,取决于你的元组有多少个域,每个域都必须对应一个参数,如果参数是scalar就是精确匹配,如果是列表就是部分匹配,如果是*就表示任意匹配。
- sum(pattern):tuplelist没有这个方法,只有tupledict有,将满足特定模式的变量相加。
- prod(coeff, pattern):将满足pattern的变量和coeff挑出来相乘求和,注意coeff(一般用dict,将下标索引映射为某一个值)必须和调用prod的tupledict的结构(索引)一致。
Gurobi的常量、回调码、参数名、属性名和状态名记不住怎么办
Gurobi提供了一个GRB类专门用来解决这个困扰——怎么解决的呢?就是把一些不好记的常量或字符串用一个名字好记的变量(类属性和实例属性)存储起来。GRB的类属性定义了一系列常量值对应变量,GRB的实例属性Callback中定义了常用的where和what码对应变量,Attr定义了常用属性字符串对应变量,Param定义了常用参数字符串对应变量。记这些类属性和实例属性的名字要比直接记忆数字或字符串容易一些,当然了,GRB包含的内容远不止于此,这块儿没法展开讲了,只能在使用的过程中不断积累常用的GRB中定义的变量(类属性和实例属性)。我在使用过程中常用的GRB类属性和实例属性:
- GRB.MINIMIZE
- GRB.MAXIMIZE
- GRB.CONTINUOUS
- GRB.BINARY
Gurobi的Attribute和Parameter
个人认为在使用Gurobi时不要把把Parameter翻译为参数。传统地,在python里面我们把类中定义的变量(类属性和实例属性)叫做属性,把一个函数或方法需要接收的东西叫做参数。但在gurobi里面Parameter其实是不同的含义:Gurobi里面的Parameter在python语言的意义下讲其实还是属性(Model对象的Params属性的子属性),只不过这个属性的作用是控制求解器的行为。所以说要获取或修改Parameter就像获取或修改python中对象的属性那样操作就行了。
获取和修改Parameterprint(model.Params.参数名) model.Params.参数名 = 新的参数值 # 求解策略参数的设置
对于Gurobi中的Attribute就好理解了,它和python意义上的属性是一致的,同样可以通过下面的方法获取属性或修改属性。
获取和修改属性print(某个gurobi对象.属性名) 某个gurobi对象.属性名 = 新的属性值
一般来说Parameter不涉及批量获取或修改的操作,但是Attribute会涉及,这个时候使用上面的方法就非常慢了。Gurobi提供了getAttr、setAtrr方法实现批量属性获取或批量属性设置。
批量获取和修改属性gurobi的模型对象.getAttr(属性名) # 获取模型对象的叫`属性名`的Attribute gurobi的模型对象.getAttr(属性名,对象序列) # 批量获取对象序列叫`属性名`的属性值 gurobi的模型对象.setAttr(属性名,新属性值) # 设置模型对象的叫`属性名`的属性设置为新属性值 gurobi的模型对象.setAttr(属性名,对象序列,新属性值序列或一个标量) # 把对象序列中对象的`属性名`属性的值设置为新属性值序列中对应属性或统一设置为同一个值
这块还是得积累,Attribute和Parameter太多了,我在使用过程中常用的Attribute和Parameter:
- Attribute
- 模型对象.ModelName:当前模型的name。
- 模型对象.ObjVal:当前模型的找到的incumbent。
- 变量对象.VarName:变量对象的name。
- 变量对象.Start:变量的热启动值。
- 变量对象.X:变量的取值。
- Parameter
- ModelSense:控制模型的优化方向。
- MIPFocus:求解器的求解重心。0是再找可行解和证明最优性取折中,1是侧重找可行解,2是侧重证明最优性,3是侧重bound的提升。
- MIPGap:最优性容差,Gap小于这个值的时候求解器就终止了。
- LogToConsole:是否将当前模型的求解信息输出到控制台。
- LogFile:设置将日志文件输出到哪里。
惰性约束和割平面
在说明如何在 Gurobi 中添加惰性约束和割平面之前,首先要介绍 Gurobi 的惰性更新机制和回调函数这两个概念。所谓惰性更新机制就是指用户对模型的任何更改都不会在对应代码被执行后立即完成。Gurobi此时的具体行为是将模型的修改放入一个等待队列,当且仅当——(1)update 方法被调用(2)optimize 方法被调用(3)write 方法被调用这三种情形中的任意一种被触发时模型的修改才会真正的被完成。Gurobi之所以使用惰性更新机制是因为对于求解器而言处理模型修改信息是非常耗时的,相较于修改一次就更新一次的方式,对多次修改集中更新有着更高的效率。回调函数是计算机科学领域的一个重要概念,通俗地讲就是在让函数 B 有能力完成某个定制化功能时将函数 A 作为参数传递给它,使得函数 B 在特定的条件下调用函数 A 完成定制化功能,传递给函数 B 的这个函数 A 就叫做回调函数。Gurobi 中也有这个概念,这使得我们可以在求解过程中实时查看模型的求解信息,甚至可以在求解过程中实时地修改模型的内容。在 python 中使用 Gurobi 的回调函数分为两步:
- 定义回调函数 def func_name(model,where)并实现相应的功能
- 将自己定义的回调函数作为参数传递给 optimize 方法
惰性约束和割平面约束正是基于惰性更新机制和回调函数实现的。前面提到,对于模型的任何修改都会在上述三种情形中任意一种被触发后完成,但惰性约束和割平 面约束是例外。即使上面讲的三种情形被触发,惰性约束和割平面约束也不会被加入到模型当中。也就是说 Gurobi在优化时使用的实际上是松弛了惰性约束和割平面模型,当且仅当求解器发现了新的可行解并且可行解不满足惰性约束或割平面约束时惰性约束和割平面约束才会被加入模型。基于这一特点,当面临大规模约束时,Gurobi不需要一次性添加所有约束,这可以大幅度提高模型求解的速度。
一个例子弄清楚Gurobi的建模过程
数学模型的建立
索引和集合
- depot:代指仓库。
- virtual_depot:代指仓库,区分离开仓库和到达仓库。
- O:需求的起点集合。
- D:需求的终点集合。
- K:可用车辆的集合。
- V:网络中所有节点的集合。
- A:网络中所有合法弧的集合。
- demand:各个顾客节点的需求,正数为deliver,负数为pickup。
- cordinate_x:各个节点的x坐标。
- cordinate_y:各个节点的y坐标。
- distance:合法弧之间的距离。
决策变量
:弧流变量,如果k车从i走到j那么它就取1。 : 车辆k离开i节点之后车上的载重。 :车辆k到达i节点的时间。
使用Gurobi表达并求解这个模型的步骤
- 创建一个Data类型,专门用来管理模型中涉及到的索引、集合和参数。
pdpmodel.pyfrom math import sqrt from gurobipy import * class Data: """ 维护模型中涉及到的索引、集合和参数 """ # 索引 depot = 0 virtual_depot = None # 集合 O = None D = None V = None # V=O+D+depot+virtual_depot K = None A = None # 参数 capacity = None demand = None cordinate_x = None cordinate_y = None distance = None def _initialData(cls,filename='./1_1PDPdata.txt'): """ 这个函数里面就写你要怎么样初始话上方的数据 """ infolst = [] with open(filename,'r') as f: strlst = f.readlines() flag = False # 当flag为True,就说明这一行是我们需要读取的信息 for line in strlst: if line == 'CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME\n': flag = True continue if flag and line.strip(): # 开始解析数据 infolst.append(list(map(int,line.strip().split()))) # 开始初始化上方列出的数据 number_of_demand = (len(infolst)-1)//2 cls.virtual_depot = 2*number_of_demand+1 cls.O = [i for i in range(1,number_of_demand+1)] cls.D = [i for i in range(number_of_demand+1,2*number_of_demand+1)] cls.V = [cls.depot]+cls.O+cls.D+[cls.virtual_depot] cls.K = [i for i in range(3)] cls.A = [] for i in cls.V: for j in cls.V: # 像这种网络模型,先就把合法的弧筛选出来后面可以省很多事情 # (1)自环 # (2)回起点的弧、从终点出发的弧 # (3)一点到其自身虚拟节点的弧 if i == j: continue if (j == cls.depot) or (i == cls.virtual_depot): continue if (i == cls.depot) and (j == cls.virtual_depot): continue cls.A.append((i,j)) cls.capacity = 100 cls.demand,cls.cordinate_x,cls.cordinate_y,cls.distance = {},{},{},{} for i in range(2*number_of_demand+2): if i == cls.virtual_depot: cls.demand[i] = infolst[cls.depot][3] cls.cordinate_x[i] = infolst[cls.depot][1] cls.cordinate_y[i] = infolst[cls.depot][2] else: cls.demand[i] = infolst[i][3] cls.cordinate_x[i] = infolst[i][1] cls.cordinate_y[i] = infolst[i][2] for i in cls.V: for j in cls.V: if (i,j) in cls.A: cls.distance[(i,j)] = sqrt((cls.cordinate_x[i]-cls.cordinate_x[j])**2+(cls.cordinate_y[i]-cls.cordinate_y[j])**2) def _consoleOutput(cls): """ 这里就写如何展示获取的信息,检查数据读取是否正确 """ messege = {"仓库索引":0, "需求起点集合":cls.O, "需求终点集合":cls.D, "网络包含的所有点的集合":cls.V, "单车载重":cls.capacity, "各点需求(正数deliver,负数pickup)":cls.demand, } print('-'*5+"集合和参数信息汇总表"+'-'*5) for key,value in messege.items(): print(key,'\n',value) print('-'*20)
- 创建对应的Model类,这个Model类要继承于前面创建的Data类型,这样访问索引、集合和参数要方便一点——比如,你不让Model类继承上面的Data类,而是把Data实例化之后作为Model的一个实例属性data,那么你在搭建模型的时候想要访问集合,比如说访问O,就要写成self.data.O。但如果使用继承的方式,那么就可以直接写成self.O。此外,通过这个类的实例属性来管理模型对象和变量对象,并分别实现buildModel方法用于搭建模型,实现runModel方法设置模型的Parameters从而控制模型的行为并运行求解器,实现ouputFile方法把你觉得之后可以用来分析的文件给导出去。
pdpmodel.pyclass PDPModel(Data): """ 这个类直接继承于Data,目的是使访问Data中定义的集合和参数更加简便 """ def __init__(self): # 完成数据初始化操作 super(PDPModel,self)._initialData() super(PDPModel,self)._consoleOutput() # 管理变量和辅助变量 self.x = None self.Q = None self.t = None # 模型对象管理 self.model = Model("PDP1_1") def buildModel(self): """ 这个方法用来搭建模型, (1)添加变量 (2)添加目标函数 (3)添加约束条件 """ # (1) self.x = self.model.addVars(self.A,self.K,vtype=GRB.BINARY,name='x') self.Q = self.model.addVars(self.V,self.K,lb=0,ub=self.capacity,vtype=GRB.CONTINUOUS,name='Q') self.t = self.model.addVars(self.V,self.K,lb=0,vtype=GRB.CONTINUOUS,name='t') # (2) self.model.setObjective( quicksum(self.distance[i,j]*self.x[i,j,k] for i,j in self.A for k in self.K), sense=GRB.MINIMIZE ) # (3) # 1.每个需求都必须被服务 self.model.addConstrs( (self.x.sum(i,"*","*")==1 for i in self.O), name = 'constrs1' ) # 2.车辆在i点pickup了就得在对应的n+i去deliver self.model.addConstrs( (self.x.sum(i,"*",k)==self.x.sum(len(self.O)+i,"*",k) for i in self.O for k in self.K), name = 'constrs2' ) # 3. 流平衡约束 self.model.addConstrs( (self.x.sum(self.depot,"*",k)==1 for k in self.K), name = 'constrs3_1' ) self.model.addConstrs( (self.x.sum("*",self.virtual_depot,k)==1 for k in self.K), name = 'constrs3_2' ) self.model.addConstrs( (self.x.sum(i,"*",k)==self.x.sum("*",i,k) for i in self.O+self.D for k in self.K), name = 'constrs3_3' ) # 4. 时间连贯性约束 self.model.addConstrs( (self.t[i,k]+self.distance[i,j]-9999*(1-self.x[i,j,k])<=self.t[j,k] for i,j in self.A for k in self.K), name = 'constrs4' ) # 5. 先取货后送货 self.model.addConstrs( (self.t[len(self.O)+i,k]-self.t[i,k]>=self.distance[i,len(self.O)+i] for i in self.O for k in self.K), name = 'constrs5' ) # 6. 重量连贯性约束 self.model.addConstrs( (self.Q[i,k]+self.demand[j]-9999*(1-self.x[i,j,k])<=self.Q[j,k] for i,j in self.A for k in self.K), name = 'constrs6' ) # 7. 载荷约束 self.model.addConstrs( (self.Q[i,k]>=0 for i in self.V for k in self.K) ,name = 'constrs7_1' ) self.model.addConstrs( (self.Q[i,k]>=self.demand[i] for i in self.V for k in self.K) ,name = 'constrs7_2' ) self.model.addConstrs( (self.Q[i,k]<=self.capacity for i in self.V for k in self.K) ,name = 'constrs7_3' ) self.model.addConstrs( (self.Q[i,k]<=self.capacity+self.demand[i] for i in self.V for k in self.K) ,name = 'constrs7_4' ) def runModel(self): """ 这个方法里面就来设置Parameter,控制求解器的行为,然后运行求解器 """ self.model.Params.MIPFocus = 3 self.model.Params.NonConVex = 2 self.model.params.MIPGap = 0 self.model.Params.lazyConstraints = 1 self.model.Params.LogFile = '1_1pdpd.log' self.model.optimize() def ouputFile(self): """ 这里就写你想导出哪些文件,如果只导出单一文件的话,也可以通过self.model.Params.ResultFile文件进行设置 """ self.model.write('1_1pdpd.lp') self.model.write('1_1pdpd.sol') if __name__ == "__main__": pdpmodel = PDPModel() pdpmodel.buildModel() pdpmodel.runModel() pdpmodel.ouputFile()
- 专门创建一个新的文件来分析求解结果,因为分析求解结果的过程涉及到反复调试代码,如果不单独开一个文件,那时间成本实在是太高了。
analysisfrom numpy import zeros from gurobipy import * from matplotlib import pyplot as plt # 全局read的作用是创建新模型对象 ,模型对象read的作用是向现有模型添加信息 model = read('./1_1pdpd.lp') model.read('./1_1pdpd.sol') # 把之前已经优化好的解读到变量的start属性中去,相当于做一个热启动,这样再去分析求解结果就会快很多 model.Params.IterationLimit = 1 model.optimize() # 准备几个矩阵,存放决策变量的求解结果,方便后面分析 x = zeros((18,18,3)) Q = zeros((18,3)) t = zeros((18,3)) for var in model.getVars(): if var.VarName.startswith('x'): i,j,k = map(int,var.VarName[2:-1].split(',')) x[i,j,k] = var.X if var.VarName.startswith('Q'): i,k = map(int,var.VarName[2:-1].split(',')) Q[i,k] = var.X if var.VarName.startswith('t'): i,k = map(int,var.VarName[2:-1].split(',')) Q[i,k] = var.X # 根据弧流变量打印出各个车辆的路径 def printCarRoutes(x,depot): m,n,s = x.shape car_routes = [] for k in range(s): k_car_route = [depot] for j in range(m): if x[k_car_route[-1],j,k] > 0.1: k_car_route.append(j) car_routes.append(k_car_route) print('-'*5+'路径求解结果'+'-'*5) for k,route in enumerate(car_routes): print("{}车的路径为:{}".format(k,route)) return car_routes car_routes = printCarRoutes(x,0) # 打印其它类型变量的求解结果 allQincar = [] for k in range(3): route = car_routes[k] Qincar = [] for i in route: Qincar.append(Q[i,k]) allQincar.append(Qincar) print('-'*5+'沿途载重情况'+'-'*5) for k,Qincar in enumerate(allQincar): print("{}车的载重情况为:{}".format(k,Qincar))
Gurobi证书过期处理方法
参考:https://blog.csdn.net/wengkebiao/article/details/128092906
遇到报错Set parameter Username Set parameter LicenseID to value 2603509 Traceback (most recent call last): File "D:\code repository\项目\LRHeuristics\example.py", line 11, in <module> lgmodel = Model("拉格朗日松弛模型") ^^^^^^^^^^^^^^^^^^^^^^^^^ File "src\\gurobipy\\_model.pyx", line 146, in gurobipy._model.Model.__init__ File "src\\gurobipy\\gurobi.pxi", line 60, in gurobipy._core.gurobi._getdefaultenv File "src\\gurobipy\\env.pxi", line 88, in gurobipy._core.Env.__init__ gurobipy._exception.GurobiError: License expired 2025-12-25 [Finished in 5.7s]
重新更新证书,两步就ok了:
- 上gurobi官网,点那个Named-User Academic获取命令输入到cmd当中重写获得许可证.lic文件
获取lic文件PS C:\> grbgetkey xxxx官网会给你一个码 info : grbgetkey version 12.0.0, build v12.0.0rc1 info : Platform is win64 (windows) - Windows 11.0 (26100.2) info : Contacting Gurobi license server... info : License file for license ID 2759492 was successfully retrieved info : License expires at the end of the day on 2026-12-27 info : Saving license file... In which directory would you like to store the Gurobi license file? [hit Enter to store it in C:\Users\sheyu]: C:\gurobi A license file already exists in 'C:\gurobi\gurobi.lic' Continue? [Y/n] y info : License 2759492 written to file C:\gurobi\gurobi.lic info : You may have saved the license key to a non-default location info : You need to set the environment variable GRB_LICENSE_FILE before you can use this license key info : GRB_LICENSE_FILE=C:\gurobi\gurobi.lic
- 在系统变量中加上GRB_LICENSE_FILE,值设置为你新拿到的.lic文件的地址
模型infeasible调试方法
- 用computeIIS方法找出Irreducible Inconsistent Subsystem,
你的模型.computeIIS()
你的模型.write('xxx.ils')
- 逐行注释,再添加会约束,定位到哪类约束存在问题






.png)