Callbacks功能

杉数求解器COPT提供Callbacks(回调)功能,支持用户在混合整数规划分支切割的求解过程中,获取中间信息(如当前最优下界、当前最优目标值等);以及控制求解进程:如修改模型(如添加惰性约束、添加割平面),终止求解等。目前支持使用Callbacks的问题类型有:混合整数规划、混合二阶锥规划、混合整数二次(约束)规划。(下文为避免表述冗余,我们将这些混合整数的问题类统一称为MIP)。

回调函数是COPT在求解过程中所调用的由用户自定义的函数,用户可以通过COPT支持的API为一个或多个Callback Context(回调函数触发条件)注册一个自定义的回调函数。 不同API中使用Callbacks功能 章节将会详细介绍如何设置回调函数。回调函数将在求解过程中的特定时刻被调用,取决于回调触发条件。在回调函数被调用中,用户可以分别 获取求解过程中间信息控制求解进程。可获取的信息以及可执行的操作取决于Callback Context。目前,COPT支持四种Callback Context:

  • CBCONTEXT_INCUMBENT: 当找到当前最优可行解时,触发回调函数;

  • CBCONTEXT_MIPNODE: 当处理完成MIP节点(并求解LP松弛问题完成)时,触发回调函数;

  • CBCONTEXT_MIPRELAX : 当找到MIP线性松弛解时,触发回调函数;

  • CBCONTEXT_MIPSOL: 当找到MIP可行解时,触发回调函数。

本章节的内容构成如下:

注意:

COPT一次性只支持注册一个回调函数,但是在同一个回调函数中可以传递多个Callback Context。如果用户想要在不同的Callback Context下执行不同的操作(例如在 CBCONTEXT_MIPSOL 下,添加惰性约束;在 CBCONTEXT_MIPRELAX 下,添加用户自定义的割平面约束),则需要将这些相关的Callback Context都作为函数参量传递给回调函数,在该函数内部,可以在指定的Context下,执行对应的操作。

获取求解过程中间信息

MIP求解过程中,可获取到的中间信息取决于Callback Context(Callback的触发条件),具体对应关系请参见下表。

通常,用户在回调函数内部调用API函数来获取求解中间信息(API函数的名称取决于API,如C API是 COPT_GetCallbackInfo ,面向对象的API则通过 CallbackBase.getInfo ),通过将所需信息以字符串形式传递给函数参数来指定。COPT支持获取的Callback信息,详细请参考 Callback相关信息 部分。

对应关系罗列如下表:

Callback Context

Callback Information

CBCONTEXT_MIPSOL

MipCandObj, MipCandidate

CBCONTEXT_MIPRELAX

RelaxSolution, RelaxSolObj

CBCONTEXT_MIPNODE

NodeStatus, RelaxSolution, RelaxSolObj, MipCandObj, MipCandidate

CBCONTEXT_INCUMBENT

除了上述列出的Callback信息及特定相对应的Callback Context以外, BestObj, BestBnd, HasIncumbent, Incumbent 在任何触发条件都可以被获取。

注意

  1. 如果 HasIncumbent == False , 则无法获取 Incumbent

  2. 信息 NodeStatus 的返回值为常数,表示当前节点LP松弛问题的求解状态,可取值请参考: 一般常数章节:解状态(部分)

  3. 对于 IncumbentRelaxSolution , 和 MipCandidate 这三项信息,不同接口中的获取方式有所不同:

    • C语言:通过函数 COPT_GetCallbackInfo ,需获取的中间信息名称作为函数的参数提供;

    • 面向对象的编程语言(C++/C#/Java/Python)中, CallbackBase 类提供专门的函数,以获取相应中间信息。

    例如,Python/C++接口的 CallbackBase 类提供 getIncumbentgetRelaxSolgetSolution 。其他编程语言接口类似, 可参考各API的 CallbackBase 类。

控制MIP求解进程

COPT提供相关函数,让用户在MIP分支切割法的求解过程中,交互式地添加惰性约束或割平面,以控制MIP求解进程。主要有如下三类操作:

  1. 添加惰性约束

  2. 添加割平面

  3. 设置自定义的可行解

添加惰性约束

惰性约束是仅在判断约束条件被违反时才向模型添加的约束。对于一些包含大量约束的模型,这种做法可以有效地减小模型的规模,提高MIP求解效率。其中一个常 见问题示例是TSP模型,可以参考安装包中 "examlpes" 下的 "cb_ex1"

COPT支持两种方式添加惰性约束。一种是 在求解之前 显式向原始模型中添加惰性约束;另一种是通过用户自定义的回调函数, 在求解过程中 动态添加 惰性约束。为此,每个API都提供了两组方法:一组方法用于将惰性约束添加到初始模型中,另一组则用于从回调中添加惰性约束。 在C API中,这两组方法可以根据函数名称是否包含 "Callback" 来区分,例如, COPT_AddLazyConstrCOPT_AddCallbackLazyConstr 。 在面向对象的API中,这两组函数分别属于 Model 类和 CallbackBase 类。以Python为例:

  • 在求解之前,用户可以通过调用 Model.addLazyConstr()Model.addLazyConstrs() 直接向模型添加惰性约束。

  • 在求解过程中,(在当前Callback Context支持的情况下)用户可以通过回调函数内部的 CallbackBase.addLazyConstr()CallbackBase.addLazyConstrs() 动态添加惰性约束。

对于以上两种方式,COPT会将添加的惰性约束单独存储,与实际优化模型分开,并且只有在求解过程中发现的解违反了这些约束时,才会将它们添加到模型中。

为确保正确性,在求解过程中,COPT会检查到目前为止所添加的全部惰性约束是否被任何找到的解违反,这将会增加求解时间(特别是当添加了许多未被违反的惰性约束时)。 建议用户只在必要时添加惰性约束,例如当它们被找到的解所违反时。

惰性约束只能在 CBCONTEXT_MIPSOLCBCONTEXT_MIPRELAX 中添加。虽然严格来说不需要检查每个LP松弛解是否违反了惰性约束,但用户必须 针对 CBCONTEXT_MIPSOL 下所找到的每个可行解,检查其是否违背了惰性约束,以避免产生错误结果。

为了避免添加过多不必要的惰性约束,COPT实施了一些简单的冗余检查,完全重复惰性约束将被丢弃。即便如此,添加许多非常相似但冗余的惰性约束会对COPT的性能 产生负面影响,用户应避免这种情况。

注意:

  • 如果用户为 CBCONTEXT_MIPSOL 注册一个回调函数,那么COPT将会认为用户需要添加惰性约束。由于惰性约束实际上并不是模型的一部分,这将导致 COPT在预处理期间停用对偶约减,因为对偶参数依赖于对模型中所有行(约束)的信息。

    如果用户并不打算添加惰性约束,但仍然想使用 CBCONTEXT_MIPSOL,COPT提供了 LazyConstaints 参数, 该参数使得用户可以明确告诉COPT是否将惰性约束添加到模型中。默认情况下,此参数设置为 -1 ,表示如果模型中包含惰性约束或注册了 CBCONTEXT_MIPSOL 的回调函数,COPT将会在预处理期间关闭对偶约减。将参数显式设置为 0 ,表示即使存在惰性约束或 CBCONTEXT_MIPSOL 的回调函数。将允许COPT在预处理期间进行对偶约减,这仅在非常罕见的情况下起作用。例如,如果在回调函数仅打印找到的可行解信息,但并不添加惰性约束这种 情况。然而,一旦用户添加了惰性约束,这可能导致错误的结果。如果需要打印关于模型解的信息,请考虑使用 CBCONTEXT_INCUMBENT

  • 如果用户在 CBCONTEXT_MIPSOL 中添加惰性约束,无论实际上添加的惰性约束是否被违反,当前的MIP可行解都将被拒绝。 当找到解不符合要求的情况下,这种设计使得用户能够通过添加空的惰性约束来拒绝任意可行解。但请注意,如果没有提供具体的惰性约束,COPT可能会多次 找到相同的解。如果在 CBCONTEXT_MIPRELAX 中添加惰性约束,当前的LP松弛解不一定会被拒绝,只有在实际违反了这些约束时才会被拒绝。

  • 对于面向对象的编程接口来说,在回调函数中调用 Model 类的任何函数来添加惰性约束是无效的(对于C接口来说,则是对应的API函数)。更一般的说, 在求解过程中不能更改模型,除非是添加惰性约束或切割平面。

添加割平面

在求解过程中添加割平面到模型中可以加强LP松弛模型的效果,例如削减取值为小数的LP解以改善MIP问题的下界。

COPT支持在求解过程中向模型中添加用户自定义的割平面。与惰性约束相类似,割平面可以在 求解之前 或者通过callback 在求解期间 添加到模型中。 每个API提供两组方法,一组用于向初始模型中添加割平面,另一组用于通过回调函数添加割平面。在C API中,可以根据函数名是否包含 "Callback" 进行区分, 例如, COPT_AddUserCutCOPT_AddCallbackUserCut ;在面向对象的API中,这两组函数分别对应于 Model 类和 CallbackBase 类。 以Python为例:

  • 在求解之前,用户可以通过调用 Model.addUserCut()Model.addUserCuts() 直接向模型添加割平面。

  • 在求解过程中,(在当前Callback Context支持的情况下)用户可以通过回调函数内部 CallbackBase.addUserCut()CallbackBase.addUserCuts() 动态地添加割平面。

割平面 只能CBCONTEXT_MIPRELAX 中添加。在此处,用户基于当前的LP松弛解以分离自定义的割平面。

注意

  • COPT会舍弃没有违背当前LP松弛解的割平面。

  • 对于面向对象的编程接口来说,在回调函数中调用 Model 类的任何函数来添加割平面是无效的(对于C接口来说,则是对应的API函数)。更一般的说, 在求解过程中不能更改模型,除非是添加惰性约束或割平面。

设置自定义的可行解

COPT支持在MIP求解过程中添加可行解。这使得用户能够在COPT求解过程中同步提供任何他们自己找到的可行解。例如在自行实现的启发式算法中。 已知的可行解可以通过调用 COPT_AddMipStart (参见:ref:chapMipstart)提供或者在回调函数中提供。

如果用户事先知道一个现成的解,则最好将其作为MIP的起始解提供。对于在求解过程中找到的解,在C API中,可以在回调函数内部调用 COPT_AddCallbackSolution 来逐个进行添加,在面向对象的API中,添加解所需的函数由 CallbackBase 类提供,需要依次调用两个函数: 以Python为例:

  • 设置自定义的可行解:CallbackBase.setSolution(vars, val)

  • 将自定义的解加载到模型中:CallbackBase.loadSolution()

注意

目前,通过callbacks的方式,COPT支持设置完整的可行解。

不同API中使用Callbacks功能

以面向对象的编程接口为例,展示在不同的API中,调用Callback功能的基本步骤如下:

  1. 构建一个自定义Callback类,并继承 CallbackBase 类;

  2. 实现 CallbackBase.callback() 函数,该函数为COPT调用的回调函数。在其中用户可以在不同的Callback Context下调用执行所需操作 (获取中间信息或者添加惰性约束/割平面等);

  3. 新建自定义的Callback实例;

  4. 通过Model类的 Model.setCallback() 注册Callback实例,并将Callback Context作为参数输入。如果需要为多个Context注册回调函数, 可以使用按位或运算符连接。例如, COPT.CBCONTEXT_MIPSOL | COPT.CBCONTEXT_MIPNODE

在后续的求解过程中,用户实现的 allbackBase.callback() 函数将在每个被注册的Context中被调用。用户可以通过调用 CallbackBase 类的方法 where() 以获取当前调用的Context。

如前面的部分已经提到的, CallbackBase 类中的函数(或其对应的C API函数)只能在特定的Context中被调用。下表列出了每个Callback Context中 允许的特定回调操作,以Python为例, CallbackBase 类中的成员函数以及对应可调用的Context罗列如下:

Context

Function

CBCONTEXT_INCUMBENT

getInfo, getIncumbent, load/setSolution

CBCONTEXT_MIPNODE

getInfo, getIncumbent, getRelaxSol, load/setSolution

CBCONTEXT_MIPRELAX

addUserCut(s), getIncumbent, getInfo, getRelaxSol, load/setSolution

CBCONTEXT_MIPSOL

addLazyConstr(s), getIncumbent, getInfo, getSolution, load/setSolution

注意:

  • 虽然 getInfo 可以在所有Callback Context中被调用,但可用的信息依然取决于Callback Context。详细信息请参阅 获取求解过程中间信息

  • 在除Python以外的其他API中,getInfo 经常被拆分为 getIntInfogetDblInfo 以获取不同数据类型的回调信息。

  • 上述函数在不同的API中可能名称略有不同,但呈现的关系是相同的。

虽然上述步骤以Python API中作为参考,但对于面向对象编程语言API中,实现方式都类似,用户可以参考安装包 "examlpes" 目录下的的示例代码。 如Python接口的 "cb_ex1.py" 。对于C API,在实现和注册回调时的主要区别如下:

  • 自定义回调函数可以是任何函数,其函数名称为 int COPT_CALL <function>(copt_prob* prob, void* cbdata, int cbctx, void* usrdata), 其中 <function> 是任意的函数名。

  • 获取当前触发条件不再使用 where(),而是在 cbctx 参数中传递回调触发条件。

  • 可以通过定义一个自定义的 struct 结构并将其作为 usrdata 参数传递来传递与回调相关的信息。

具体实现请参考C API示例文件夹中的 cb_ex1.c

回调函数在不同编程接口中的调用方式和函数名称 略有不同 ,但是支持的功能和函数含义是一致的,请进一步参考不同编程接口API参考手册的对应章节以获取具体介绍: