COPT和Gurobi语法区别
[Gurobi关停中国地区使用](https://www.gurobi.com/cn/ty/academics)
比较遗憾,Gurobi将从即日起停止对中国地区学术许可的申请审批。是时候跟Gurobi说拜拜,拥抱国产求解器COPT了。 这篇文章主要是总结一下COPT和Gurobi的区别,希望能够以最小的学习成本完成框架的迁移。 因此,这篇文章并不是关于COPT的详细教程,只会大致记录COPT和Gurobi语法或使用上存在区别的地方。 没有提到存在区别的地方,暂时还是按照Gurobi的语法写,报错了再查。 关于Gurobi的使用,参考:点这里。
- COPT中管理各种常量的类是COPT类,而不是GRB类
- COPT的求解器调用是solve方法,而不是optimize方法
- COPT中参数通过Param属性访问,而不是Params属性
- COPT中控制控制台输出的参数是Logging,而不是LogToConsole
- COPT中控制线性规划问题用什么算法求解的参数是LpMethod,而不是Method
- COPT模型对象的getAttr、setAttr方法不支持批量获取和设置其它对象的属性值,只能自己写推导式
- COPT的Column对象构造列的语法和Gurobi的Column对象在参数顺序上相反
- COPT调用computeIIS计算不可行原因后不能直接用write方法写ils文件,得用writeIIS方法写
- COPT使用addVars创建变量,变量名设置是nameprefix而不是name
- COPT使用addConstrs批量添加约束,变量名设置是nameprefix而不是name
- COPT必须显示地创建模型所用的环境,不像Gurobi的Model会使用默认环境
- COPT的变量名属性是Name,而不是VarName
- COPT重置参数为默认值用的是resetParam而不是resetParams,这个方法有bug…
- COPT判断变量是否位于最优基当中用的是Basis属性,而不是VBasis属性
我让AI更具Gurobi和COPT的区别,修改之前写的Gurobi的教程,得到COPT的教程:
本文与Gurobi教程的关系
这篇是《一篇文章弄懂GUROBI基础操作》的COPT对照版。COPT(杉数科技 Cardinal Optimizer)的Python接口coptpy在设计上和gurobipy高度相似(连tupledict都有),所以绝大多数代码只需要“改写法、不改思路”就能迁移。先记住COPT与Gurobi最大的几处写法差异(本文实测基于COPT v8.0.6):
- 模型求解方法叫
solve(),不叫optimize(); - 求解器参数是
model.Param.xxx(Gurobi是model.Params.xxx,COPT里没有Params); - 批量建变量用
addVars(..., nameprefix='x'),变量名自动生成形如x(1,2,0)的圆括号风格(Gurobi是方括号x[1,2,0]),而且addVars不认name这个关键字; - 属性名:模型级
ObjVal/Status两种大小写都兼容;变量/约束级取值、检验数、对偶、基态官方习惯用小写x、rc、pi、basis; - 建模惯例不同:gurobipy里直接
Model("名字")建模型;coptpy官方写法是先建环境env = Envr()再用env.createModel(name='...')建模型;coptpy也没有Gurobi那种顶层全局read()函数,读模型文件要“先建空模型,再调用模型对象的read('xxx.lp')”。
COPT特有的数据结构
在coptpy里面同样会经常和tuplelist和tupledict这两种数据结构打交道,而且用法和gurobipy基本一致——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存储的,那么我们可以直接像下面这么写。
是不是一下子就简洁许多?这就是tuplelist和tupledict这两个数据结构设计的核心逻辑,总结一下,你只需要知道tuplelist和tupledict可以让我们快速实现筛选、求和等操作。具体而言,这两个数据结构在他们的爸爸list和dict的基础上额外实现了下面这些方法。 - select(pattern):对于tuplelist,返回满足pattern的所有元组构成的tuplelist,对于tupledict,返回满足pattern的所有变量及其对应索引构成的tupledict。置于这个pattern,取决于你的元组有多少个域,每个域都必须对应一个参数,如果参数是scalar就是精确匹配,如果是列表就是部分匹配,如果是\*就表示任意匹配。 - sum(pattern):tuplelist没有这个方法,只有tupledict有,将满足特定模式的变量相加。注意:pattern里的参数个数必须和变量的索引维度一致,比如变量下标是三元组`(i,j,k)`,就得写`x.sum("*","*","*")`这种三个占位符的写法。 - prod(coeff, pattern):将满足pattern的变量和coeff挑出来相乘求和,注意coeff(一般用dict,将下标索引映射为某一个值)必须和调用prod的tupledict的结构(索引)一致。 **和Gurobi的差异**:gurobipy里常用 `model.addVars(..., name='x')` 让变量自动叫 `x[1,2,0]`;coptpy里这个关键字叫 `nameprefix`(注意全是小写),生成的变量名是 `x(1,2,0)`(圆括号)。好在两者去掉首尾字符后内部都是 `1,2,0`,所以写“按变量名解析下标”的代码时几乎可以原样照搬。如果用tupledictx.sum("*",[1,2])
# COPT的常量、参数名、属性名和状态名记不住怎么办 COPT提供了COPT类和gurobipy的GRB类扮演一样的角色——把一些不好记的常量或字符串用一个名字好记的变量(类属性)存储起来。除此之外,COPT.Param(注意大小写:是 `COPT.Param` 而不是 `coptpy.Param`)里定义了常用的参数名字符串,`COPT.attr` 里定义了常用的属性名字符串。记这些类属性的名字要比直接记忆数字或字符串容易一些。我在使用过程中常用的COPT类属性: - COPT.MINIMIZE / COPT.MAXIMIZE(优化方向) - COPT.CONTINUOUS / COPT.BINARY / COPT.INTEGER(变量类型) - COPT.OPTIMAL / COPT.INFEASIBLE / COPT.UNBOUNDED(求解状态码,求解后和 `model.Status` 比较用) - COPT.LESS_EQUAL / COPT.GREATER_EQUAL / COPT.EQUAL(约束方向,其实直接用 `<=` `>=` `==` 就行) - COPT.BASIS_BASIC / COPT.BASIS_LOWER / COPT.BASIS_UPPER(变量的基态常量,和 `var.basis` 的返回值比较) # COPT的Parameter和Attribute 沿用Gurobi教程里对这两个词的区分:COPT里的Parameter是控制求解器行为的一类属性,在python语言的意义下它仍然是Model对象的属性(挂在 `model.Param` 这个子对象上),所以获取或修改Parameter就像修改python对象的属性一样操作就行了。注意Gurobi那篇里写的是 `model.Params`,COPT这里是 `model.Param`(没有s),两种拼写 `model.Param` 和 `model.param` 都兼容。addVars返回的就是tupledictfrom coptpy import * env = Envr() model = env.createModel(name='demo') A = [(1,2),(1,3),(2,3)] # 两个节点之间的合法弧 K = [0,1] # 两辆车 x = model.addVars(A, K, vtype=COPT.BINARY, nameprefix='x') # 下标为(i,j,k) print(x[1,2,0]) # 返回对应的Var对象 print(x.sum(1,"*","*")) # 弧从节点1出发的所有x_{1,j,k}之和
对于COPT中的Attribute也是一样的,和python意义上的属性一致,直接点出来就能读:获取和修改Parameterprint(model.Param.TimeLimit) model.Param.TimeLimit = 60 # 求解策略参数的设置 model.setParam('Threads', 8) # 也可以走setParam,效果一样 print(model.getParam('Threads'))# 读取参数
**COPT与Gurobi属性写法上的差异**(常用部分,实测): | 含义 | Gurobi | COPT | |---|---|---| | 当前最优目标值 | `model.ObjVal` | `model.ObjVal` 或 `model.objVal`(两种大小写都行) | | 求解状态 | `model.Status` | `model.Status` 或 `model.status` | | 变量名 | `var.VarName` | `var.name` 或 `var.Name` | | 变量取值 | `var.X` | `var.x` 或 `var.X` | | 检验数 | `var.RC` | `var.rc`(官方示例用小写) | | 变量是否在最优基 | `var.VBasis == GRB.BASIC` | `var.basis == COPT.BASIS_BASIC` | | 约束对偶值 | `constr.Pi` | `constr.pi`(官方示例用小写) | | 约束松弛量 | `constr.Slack` | `constr.slack` 或 `constr.Slack` | | 模型方向 | `model.ModelSense` | `model.ObjSense`(建模时用`setObjective(..., sense=COPT.MINIMIZE)`) | | 模型名 | `model.ModelName` | `model.getName()` / `model.setName('xxx')` | 几个必须注意的点:获取属性print(模型对象.属性名) print(变量对象.属性名)
rc(检验数)和pi(对偶值)只有在模型以LP求解(比如连续模型,或者MIP根节点LP)之后才能读,MIP求解结束后读取会报错,Gurobi同理;- COPT没有Gurobi那种
model.getAttr(属性名, 对象序列)/model.setAttr(...)的批量读写,变量级批量取值直接用列表推导,基态批量取值用model.getVarBasis():xval = [var.x for var in model.getVars()] # 批量取X vbasis = model.getVarBasis() # 批量取基态 pi = [c.pi for c in model.getConstrs()] # 批量取对偶 - 求解状态比较、判断用:
if model.Status == COPT.OPTIMAL:。
常用Parameter对照(Gurobi的常用参数在COPT里哪些同名、哪些要换):
| Gurobi | COPT | 备注 |
|---|---|---|
Method=2(内点法) |
LpMethod=2 |
COPT中LpMethod=2即内点法(配合默认crossover能给出基解与对偶) |
MIPGap=0 |
RelGap=0、AbsGap=0 |
COPT把相对/绝对Gap拆成两个参数 |
NonConVex=2 |
NonConvex=2 |
注意拼写,COPT里没有NonConVex |
lazyConstraints=1 |
LazyConstraints=1 |
惰性约束开关 |
Heuristics |
HeurLevel |
控制启发式强度 |
LogToConsole=0 |
Logging=0 |
实测COPT的LogToConsole并不负责关闭控制台输出,想静默求解要设Logging=0 |
MIPFocus/BestObjStop/PoolSolutions/NodefileStart/DualReductions/Cutoff |
无同名参数 | COPT的求解重心、解池、内存管理等功能入口不同,参考官方参数文档(TimeLimit、Threads、NodeLimit、Presolve、MemLimit等通用参数都存在) |
惰性约束和割平面
和Gurobi一样,COPT也采用惰性更新机制——对模型的修改并不立即生效,而是进入等待队列,直到solve()、write()、update() 等操作被调用时才真正把修改提交给求解器(注意方法名:COPT求解是solve(),不是Gurobi的optimize())。基于这一点,建模时一口气加很多约束、然后统一求解,效率比改一次更新一次高得多。
COPT同样支持回调(模型对象上有setCallback方法,把回调函数挂到求解过程上)和惰性约束/割平面(参数LazyConstraints配合回调使用),原理和Gurobi那篇里讲的完全一致:优化时求解器使用的是“松弛了惰性约束/割平面”的模型,当且仅当新可行解违背了这些约束时才把它们加进模型。差异主要在回调的触发码(where/what码)和回调接口的具体写法上,直接对照官方文档的回调示例即可,本文不再展开。



