简介:Bonmin-master 是面向运筹优化、工程计算与科研开发者的开源混合整数非线性规划求解库源码包,适合需要处理整数约束与非线性函数耦合问题的中高级用户。Bonmin 基于 LP/NLP 的分支定界算法,将问题逐步分支并估计上下界,以缩小搜索空间逼近全局最优解,可应用于生产计划、物流调度、经济建模等场景。压缩包约 950KB,共 300 个文件,以 98 个 cpp 与 91 个 hpp 构成核心算法实现,辅以 25 个 in、19 个 tex 及 17 个 am 等构建与文档文件,并含 configure、makefile、install 等安装脚本,便于在本地编译部署。目前已有 610 人学习下载。通过该源码包,读者可获取 Bonmin 主分支的完整代码结构,理解分支定界与 LP/NLP 求解器的协作方式,并借助安装脚本与依赖说明完成环境配置,为实际 MINLP 问题提供可扩展的求解基础。
1. Bonmin 混合整数优化:从源码编译到第一个 MINLP 求解
如果你手头有一个目标函数或约束里带非线性项、同时又要求部分变量取整数的问题,比如化工过程设计里选设备台数、能源调度里决定机组启停、或者排产里定批次数量,那它大概率是一个 MINLP(混合整数非线性规划)。这类问题用纯 LP/MILP 求解器搞不定非线性,用纯 NLP 求解器又处理不了整数变量,而 Bonmin 就是专门填这个空档的开源求解器。它基于 COIN-OR 生态,支持凸和非凸 MINLP,内置 B-BB、B-OA、B-QG、B-Hyb 等多种分支定界与外包络算法。这篇笔记按“先编译装好、再跑通第一个模型、最后调参避坑”的顺序走一遍,适合刚接触 MINLP、准备把 Bonmin 集成进自己求解流程的工程师。
2. 编译 Bonmin 前必须想清楚的依赖链与目录规划
Bonmin 不是一个能pip install就完事的库,它依赖一长串 COIN-OR 基础组件,编译顺序错了或者依赖版本对不上,报错会非常隐蔽。这一章先把依赖关系和目录规划讲透,再给可复现的编译命令。
2.1 Bonmin 到底依赖哪些库,为什么不能跳步
Bonmin 的核心依赖分三层。最底层是基础工具库:CoinUtils(数据结构与工具函数)、Osi(求解器抽象接口)。中间层是它直接调用的算法库:Cbc(整数规划分支切割)、Cgl(切割生成)、Clp(线性规划单纯形)、Ipopt(内点法非线性求解器)。最上层才是Bonmin本身,它把 MILP 的分支框架和 NLP 的连续求解能力拼在一起。
关键点在于:Bonmin 的分支定界过程,每个节点上要解一个 NLP 松弛,这个松弛交给 Ipopt;而整数分支逻辑和切割则复用 Cbc/Cgl。所以 Ipopt 必须能正常工作,且它自己又依赖HSL或MUMPS做稀疏线性代数。很多人编译 Bonmin 失败,根因不在 Bonmin,而在 Ipopt 的线性求解器没配好。
常见做法是用 COIN-OR 官方的coinbrew脚本自动拉取和编译整条依赖链,它能按正确顺序处理。但如果你所在环境网络受限或需要固定版本,就得手动按顺序编译。我一般会手动编,因为这样每个库的编译选项可控,出问题好定位。
2.2 目录规划与编译顺序
先规划一个干净的目录,把源码、编译产物、安装路径分开,避免污染系统目录:
# 目录规划:源码、构建、安装三分离 export COIN_ROOT=$HOME/coin # 总根目录 mkdir -p $COIN_ROOT/src # 源码 mkdir -p $COIN_ROOT/build # 编译中间产物 mkdir -p $COIN_ROOT/install # 最终安装路径 export PREFIX=$COIN_ROOT/install编译顺序严格按依赖从底到顶:CoinUtils → Osi → Clp → Cgl → Cbc → Ipopt → Bonmin。下面以 CoinUtils 为例给出通用编译模板,其余库把目录名换掉即可:
cd $COIN_ROOT/build mkdir -p CoinUtils && cd CoinUtils # 配置:指定安装前缀,关闭不必要的共享库以简化部署 $COIN_ROOT/src/CoinUtils/configure \ --prefix=$PREFIX \ --disable-shared \ --enable-static make -j$(nproc) # 并行编译,核数按机器调整 make install # 安装到 PREFIX逻辑说明:--prefix决定make install后头文件和库落到哪里,后续库配置时要用--with-coinutils-incdir之类参数指过来。--disable-shared --enable-static生成静态库,部署时不用配LD_LIBRARY_PATH,少一类玄学问题。-j$(nproc)用满 CPU 核数加速,内存小的机器把核数调低,否则链接阶段可能 OOM。
Ipopt 编译时要额外指定线性求解器。用 MUMPS 的话,配置里加--with-mumps并给出 MUMPS 库路径;用 HSL 则加--with-hsl-lib和--with-hsl-incdir。这一步配错,Ipopt 能编过但一运行就报线性求解失败。
2.3 Bonmin 自身的配置与验证
依赖全部装好后,编 Bonmin:
cd $COIN_ROOT/build mkdir -p Bonmin && cd Bonmin $COIN_ROOT/src/Bonmin/configure \ --prefix=$PREFIX \ --disable-shared \ --enable-static \ --with-coinutils-incdir=$PREFIX/include/coin \ --with-osi-incdir=$PREFIX/include/coin \ --with-clp-incdir=$PREFIX/include/coin \ --with-cbc-incdir=$PREFIX/include/coin \ --with-ipopt-incdir=$PREFIX/include/coin make -j$(nproc) make install参数说明:每个--with-xxx-incdir把对应库的头文件目录指给 Bonmin 的构建系统,路径里的coin子目录是 COIN-OR 默认安装布局。如果某个库装到了非标准路径,这里必须显式指定,否则 configure 阶段会报找不到头文件。
验证安装是否成功,最直接的办法是跑 Bonmin 自带的示例。安装后$PREFIX/bin下会有bonmin可执行文件,$PREFIX/share下通常带示例模型文件(.nl 格式)。用命令行跑一个:
# 用 Bonmin 求解一个 .nl 格式的 MINLP 模型 $PREFIX/bin/bonmin $PREFIX/share/coin/doc/Bonmin/examples/example.nl如果输出里能看到迭代日志、目标函数值、以及 “Optimal solution found” 之类的结束状态,说明整条链路通了。看不到示例文件就自己写一个小模型,下一章讲怎么从代码里调。
3. 用 Bonmin 求解第一个 MINLP:建模、调用与结果解读
编译通过只是第一步,真正要用起来得知道怎么把问题喂给它。Bonmin 有两种使用方式:命令行读.nl文件,或者作为库链接进自己的 C++/Python 程序。这一章两种都讲,重点放在库调用,因为实际项目里几乎都是嵌进代码。
3.1 通过 C++ 接口构造并求解一个 MINLP
Bonmin 提供BonminCbc之类的求解器类,但更常用的是通过BonminSetup配置后调用。下面是一个最小可运行示例,求解一个带整数变量的非线性问题:
#include "BonBonminSetup.hpp" #include "BonOsiTMINLPInterface.hpp" #include "CoinPackedMatrix.hpp" using namespace Bonmin; int main() { BonminSetup bonmin; // 初始化:加载默认选项,准备求解环境 bonmin.initialize(); // 这里省略 TNLP 子类定义:需要实现 get_nlp_info / get_bounds_info // / eval_f / eval_grad_f / eval_g 等虚函数,描述你的问题 // 假设 MyNLP 已实现上述接口 // MyNLP* nlp = new MyNLP(); // bonmin.setPriorities(...); // 可选:设置变量分支优先级 // 调用求解 // bonmin.optimize(...); return 0; }逻辑说明:BonminSetup是配置中枢,initialize()会读取默认参数并建立内部求解器栈。真正的问题描述要靠继承TNLP(来自 Ipopt)并实现那几个虚函数:get_nlp_info告诉求解器变量和约束数量,get_bounds_info给变量上下界和整数标记,eval_f/eval_grad_f算目标函数值和梯度,eval_g/eval_jac_g算约束值和雅可比。整数变量在get_nlp_info里通过index_style和变量类型数组标记。
参数说明:变量类型数组里,INTEGER表示整数变量,CONTINUOUS表示连续变量。分支优先级通过setPriorities设置,优先级高的变量先分支,对求解速度影响很大,后面避坑章会展开。
3.2 用 Python 通过 Pyomo 调用 Bonmin
纯 C++ 写 TNLP 比较繁琐,实际项目里更常见的是用 Pyomo 建模,再让 Pyomo 调 Bonmin。前提是 Bonmin 可执行文件在 PATH 里,或者通过SolverFactory指定路径:
from pyomo.environ import ConcreteModel, Var, Objective, Constraint, SolverFactory from pyomo.environ import NonNegativeReals, Integers, value m = ConcreteModel() # 整数变量 x,连续变量 y m.x = Var(domain=Integers, bounds=(0, 10)) m.y = Var(domain=NonNegativeReals, bounds=(0, 5)) # 非线性目标:x^2 + y^2 m.obj = Objective(expr=m.x**2 + m.y**2) # 非线性约束:x * y >= 3 m.con = Constraint(expr=m.x * m.y >= 3) # 指定 Bonmin 可执行文件路径 opt = SolverFactory('bonmin', executable='/path/to/install/bin/bonmin') results = opt.solve(m, tee=True) # tee=True 打印求解日志 print('x =', value(m.x), 'y =', value(m.y)) print('objective =', value(m.obj))逻辑说明:Pyomo 把模型写成.nl文件传给 Bonmin,Bonmin 求解后把结果写回,Pyomo 再解析。tee=True让求解器日志直接打到终端,调试时必开。executable参数在 Bonmin 不在 PATH 时指定绝对路径。
参数说明:domain=Integers标记整数变量,bounds给上下界。目标函数和约束里的非线性表达式由 Pyomo 自动求导并生成.nl文件,不需要手写梯度。如果模型规模大,生成.nl文件本身可能成为瓶颈,这时要考虑用pyomo的--symbolic-solver-labels等选项优化。
3.3 结果状态码怎么读
Bonmin 返回的状态码直接决定你下一步该干什么。常见状态:
| 状态 | 含义 | 处理建议 |
|---|---|---|
| Optimal | 找到最优解 | 直接用,可做敏感性分析 |
| Feasible | 找到可行解但未证明最优 | 检查 gap 设置,可能需放宽时间限制 |
| Infeasible | 问题不可行 | 检查约束是否矛盾 |
| Unbounded | 无界 | 检查变量下界是否缺失 |
| LimitReached | 达到迭代/时间上限 | 调大 max_iter 或 max_cpu_time |
看到Feasible不要当成Optimal用,两者在工程决策里差别很大。Feasible意味着还有可能存在更优解,只是求解器没时间或没迭代次数继续找了。
4. Bonmin 求解慢、报错、结果不对的排查清单
这一章是血泪经验集中区。Bonmin 的报错信息经常指向底层库而不是真正的问题所在,下面 5 条是我踩过最多次的。
4.1 现象:一运行就报线性求解器初始化失败
原因:Ipopt 编译时没正确链接 MUMPS 或 HSL,或者运行时找不到对应的动态库。解决:重新配置 Ipopt,确认--with-mumps路径正确;如果是动态库,用ldd检查libipopt.so的依赖是否都能解析。静态编译能规避大部分这类问题。
4.2 现象:求解进度极慢,几小时不收敛
原因:分支策略和节点选择策略不适合当前问题结构。解决:换算法。Bonmin 默认用 B-BB,对非凸问题可以试 B-Hyb 或 B-OA。在选项里设bonmin.algorithm B-Hyb。另外给整数变量设分支优先级,把对目标影响大的变量排前面,能显著减少分支节点数。
4.3 现象:结果里整数变量取了小数
原因:变量类型没标记成整数,或者标记了但求解器在松弛阶段返回了连续解而你没检查。解决:确认get_nlp_info里变量类型数组正确;Pyomo 里确认domain=Integers;求解后检查value(m.x)是否接近整数,不接近说明求解器没把它当整数处理。
4.4 现象:报 NLP 求解失败,但同一个问题用 Ipopt 单独跑能收敛
原因:Bonmin 在分支节点上给 Ipopt 的初值来自父节点松弛解,这个初值可能让 Ipopt 进入不可行区域。解决:设置bonmin.warm_start_init_point yes让 Ipopt 用热启动;或者给变量更紧的界,缩小搜索空间。也可以在 TNLP 里提供更好的初始点。
4.5 现象:编译时 configure 报找不到某个库
原因:依赖库装到了非标准路径,或者--with-xxx-incdir指错了层级。解决:确认头文件实际位置,COIN-OR 默认装在$PREFIX/include/coin,配置时指到coin这一层,不是include。用find $PREFIX -name "CoinUtilsConfig.h"定位实际路径再填。
5. 让 Bonmin 在工程里真正可用的三个进阶习惯
第一个习惯是给每个模型设时间上限和 gap 容差。工程问题很少需要证明全局最优,设bonmin.max_cpu_time 300和bonmin.allowable_gap 0.01,5 分钟拿到 1% 以内的解,比等 5 小时拿精确解划算得多。第二个习惯是保存求解日志和中间解,Bonmin 支持把每个可行解写出来,出问题时能回溯是哪个节点开始跑偏。第三个习惯是先用小规模实例验证模型正确性,再放大规模,很多“求解器有问题”其实是模型本身写错了约束方向。
# 通过选项文件控制 Bonmin 行为,避免每次改代码 cat > bonmin.opt << 'EOF' max_cpu_time 300 allowable_gap 0.01 algorithm B-Hyb warm_start_init_point yes EOF # 命令行调用时加载选项文件 $PREFIX/bin/bonmin problem.nl bonmin.opt逻辑说明:选项文件把求解参数和模型解耦,换问题不用重编译。max_cpu_time单位是秒,allowable_gap是相对 gap,algorithm选分支策略。这些参数在 Pyomo 里可以通过opt.options字典传,效果一样。
我自己的习惯是每个 MINLP 项目先花半天把 Bonmin 编译和示例跑通,再花一天调参找到适合这类问题的算法组合,后面就顺了。跳过编译验证直接上大模型,翻车概率极高。希望帮到你。
本文还有配套的精品资源,点击获取