MINLP与Bonmin:开源求解器从算法原理到编译调用的完整指南
简介Bonmin-master 是为求解混合整数非线性规划MINLP问题而准备的开源代码包面向科研人员、算法工程师以及需要处理整数变量与非线性约束的工程应用者可覆盖工程、经济、物流等优化场景。资源共300个文件、约950KB以98个cpp和91个hpp源码文件为主体辅以tex文档、am/configure等自动构建脚本涵盖Bonmin主分支的核心实现、依赖库接口与安装配置所需内容。已有610人学习下载。内容基于LP/NLP求解器与分支定界算法能处理乘法、指数、对数等非线性函数通过分支和界估计逐步缩小搜索空间bonmininstall 提供安装指引用户可据此完成依赖配置、编译和调用。对算法研究与二次开发者而言源码目录和makefile体系划分清晰便于追踪分支策略、节点处理及界估计等关键细节同时支持用户按需编译与二次开发。1. 当整数变量撞上非线性目标Bonmin 到底解决什么问题如果你建的优化模型里同时出现了“选不选”“开不开”“用不用”这类 0-1 变量又有 x²、log、exp 这类非线性项那么常见的 Gurobi 和 SCIP 会直接拒绝一部分非线性函数scipy 的 minimize 又拿整数变量没办法。这类问题属于混合整数优化里最难啃的一支——混合整数非线性规划缩写 MINLP而 Bonmin 就是专门为它设计的开源求解器。Bonmin 由 COIN-OR 社区维护底层调用 Ipopt 解连续松弛、用 Cbc 处理分支过程中的线性主问题对凸 MINLP 有比较完整的最优性保障对非凸问题也能当启发式用。这篇笔记围绕 Bonmin-master 源码包从算法选型、编译安装、建模调用一路写到高频踩坑点目标是让读者拿到一条可复现的完整路径。2. 为什么 MINLP 需要 Bonmin 这类专用求解器而不是直接上 MILP 或 NLP2.1 MINLP 的数学模型连续松弛可行却给不出整数解一个典型的 MINLP 可以写成min f(x, y) s.t. g(x, y) ≤ 0 x ∈ Rⁿ y ∈ {0,1}ᵐf 或 g 至少有一个是非线性的。如果把它直接交给 NLP 求解器忽略 y 的整数约束得到的 y 通常是 0.37、0.82 这类连续值。直接四舍五入到 0 或 1大概率破坏约束条件轻则结果不可行重则差之毫厘失之千里就算运气好可行也无法证明它是全局最优。如果把它交给 MILP 求解器又要求 f 和 g 对变量都是线性表达遇到 x²、log、两个变量相乘这类项只能做分段线性近似近似误差和变量爆炸二选一。这里用一个具体场景说明。比如一条压缩空气管网的调度问题每个压缩机站要不要投运用一个 0-1 变量 y_i 表示流量 x_i 是连续变量目标里有固定启停成本 f_i·y_i 和能耗成本。能耗和流量的关系往往带二次项。更麻烦的是管网压力平衡方程里会出现流量乘阻力系数的非线性项。这就是一个天然的 MINLP手写线性化会引入大量辅助变量和 big-M 约束逼近精度还很难控。Bonmin 的处理思路是“在分支定界框架里解一连串 NLP 松弛”根节点先忽略整数约束解一次 NLP得到一个下界再按某个 y_i 的分数值把问题分成 y_i0、y_i1 两支每支继续解 NLP。整数变量被分支机制一个一个钉死到 0 或 1非线性由各个节点上的 Ipopt 负责两个难点各自由专门的组件对付。2.2 Bonmin 的三种主算法B-BB、B-OA、B-Hyb 的分工与边界Bonmin 内置三套算法骨架命令行用--bonmin.algorithm指定这也是所有使用者第一个要设的参数。B-BB非线性分支定界每个搜索树节点都直接解一个连续 NLP 松弛。下界信息准确内存占用可控但在分支节点多的时候非常慢因为每个节点都要做一次完整的 NLP 求解而 NLP 本身又需要迭代几十次牛顿型步骤。B-BB 的优势是稳健对模型的凸性依赖弱一些适合非凸问题当启发式用。B-OA外逼近把非线性项在特定点做一阶泰勒展开用这些线性化约束构造一个 MILP 主问题。MILP 负责提供下界和候选整数解NLP 子问题负责在固定整数解下精修连续变量并生成新的线性化点。好处是主问题规模小、节点收敛快坏处是外逼近对凸性要求高如果非线性项本身非凸线性化平面可能切掉最优解区域这也是新手最容易翻车的点。B-Hyb 是前两者的混合策略搜索前期用 B-OA 的 MILP 主问题快速找到好整数解后期切回 B-BB 的 NLP 分支用来补上被线性化丢掉的下界。这是 Bonmin 的默认算法对多数凸 MINLP 表现都不错。算法节点求解方式凸问题收敛速度非凸问题风险适用场景B-BB每节点一个 NLP中等下界可靠但节点多连续变量少、节点数多的模型B-OAMILP 主问题 NLP 子问题快可能切掉最优解凸性好、变量规模中等B-Hyb两阶段切换较快相对均衡默认首选先跑这个2.3 和主流求解器对比Bonmin 的定位与预期管理先说清楚Bonmin 不是拿来和 CPLEX、Gurobi 比 MILP 性能的。遇到纯线性整数规划直接用 Gurobi 或 SCIP 就好遇到无整数变量的非线性规划直接用 Ipopt。Bonmin 的定位就是两者的交集而且它内部本来就在调用 Ipopt 和 Cbc。SCIP 也支持部分非线性函数但默认对 x² 这类项做线性化或凸化处理遇到一般的非凸 MINLP 也只会给出启发式解。BARON 这类商业求解器对非凸 MINLP 有空间分支和区间算术全局性保证更强但需要商业授权。如果在开源、可自编译、能嵌入 Pyomo 和 AMPL 生态里选Bonmin 是最常用的起点。这里有一个重要的预期管理Bonmin 对凸 MINLP 给出的最优解有证明前提是问题本身满足凸性假设。实际工程模型里非线性项十有八九非凸比如两个变量相乘、三角函数、压缩机特性曲线。这时 Bonmin 更应该被当作“寻找好可行解的启发式工具”而不是全局优化器。很多初学者拿着非凸模型看到 Bonmin 输出 Feasible 就以为是最优这是后文会反复提到的误用场景。3. 从 Bonmin-master 源码包到可执行文件编译安装的最小可靠路径3.1 解开 Bonmin-master 后先检查三样东西标题里的 Bonmin-master 是 GitHub master 分支的源码打包。解压后第一件事不是运行 ./configure而是先看目录里有没有这三样东西第一ThirdParty 目录下有哪些子目录。新拉取的仓库里ThirdParty/ASL、ThirdParty/MUMPS 往往只是空目录或指向远端子模块的指针。Bonmin 依赖 ASL 读 .nl 模型文件依赖 Ipopt 提供内点算法依赖 MUMPS 做稀疏线性方程组求解。这些依赖不提前拉下来编译必挂。第二仓库根目录有没有 configure 和 Makefile.in。老版本 Bonmin 用 autotools 管理构建直接 configure 有时能走通但缺 ThirdParty 时照样报错新版本推荐用 COIN-OR 官方的 coinbrew 脚本统一管理。第三系统里有没有 gfortran 和 g。Ipopt 要编译 Fortran 写的线性代数库ASL 和 Cbc 是 C缺 gfortran 会在编译进度 80% 附近突然报错这是最典型的坑。unzip Bonmin-master.zip cd Bonmin-master ls -la ls ThirdParty/ # 预期看到 ASL、MUMPS 之类的目录名但目录里可能没有源码 # 如果看到 .gitmodules说明依赖是通过 git 子模块管理的这里要特别说明如果源码包是从 GitHub 网页的 Download ZIP 按钮下载的子模块内容不会被包含。很多人解压后直接编译失败根因就在这。所以拿到压缩包后先确认 ThirdParty 里有没有真实源码没有就走下一步的 coinbrew 流程不要浪费时间在 configure 上。3.2 用 coinbrew 拉齐依赖并编译完整命令与参数说明常见做法是先把 COIN-OR 的 coinbrew 脚本克隆到本地再用它完成 fetch、build、install 三个动作git clone https://github.com/coin-or/coinbrew cd coinbrew ./coinbrew fetch Bonminmaster --no-prompt ./coinbrew build Bonminmaster --prefix/opt/coin --test --no-prompt ./coinbrew install Bonminmaster --prefix/opt/coin逐行说明命令含义。fetch 动作是按指定分支从 COIN-OR 仓库拉取 Bonmin 源码以及全部依赖包括 Ipopt、Cbc、ASL、MUMPS。--no-prompt表示按默认路径直接拉取避免构建过程中停下来等人按回车脚本化时必加。build 动作是把拉下来的源码按依赖顺序编译--prefix指定安装前缀我习惯统一放 /opt/coin后续给 Pyomo 或其他工具引用时路径好找。--test会在构建完成后跑一套自检用例第一次接触 Bonmin 的人强烈建议开启能提前暴露链接问题。fetch 阶段最常见的异常是网络超时或某个依赖仓库暂时不可达重试两三次通常能过。build 阶段如果磁盘空间不足也会出现隐蔽错误比如 /tmp 分区太小导致临时文件写失败编译进程莫名其妙被杀掉。遇到这类问题先查磁盘不要急着改编译参数。3.3 安装完成后的自检三个命令确认求解器可用/opt/coin/bin/bonmin --version /opt/coin/bin/bonmin --help | head -30 ldd /opt/coin/bin/bonmin | grep not found--version会输出 Bonmin 以及底层 Ipopt、Cbc 的版本号确认主程序已经链接到正确的依赖版本。--help列出常用选项如果能看到--bonmin.algorithm这样的参数说明选项系统加载正常。最后一条ldd检查动态库如果有输出说明有 .so 文件没找到通常需要把 /opt/coin/lib 加进LD_LIBRARY_PATH。这一步过了可执行文件本身就没有问题了可以开始投喂模型。/opt/coin/bin/bonmin --bonmin.algorithm B-BB --print-options 21 | head -20上面这条命令相当于让 Bonmin 把当前生效的算法参数全部打印出来能正常输出就说明可执行文件的符号表加载完整可以进入建模调用环节。4. 用 Bonmin 跑通第一个混合整数优化模型Pyomo 建模与命令行参数4.1 一个用于演示的凸 MINLP 模型问题与数据为了不把问题搞复杂选一个有 3 个候选设备的产能分配问题。每个设备 i 是否启用由 0-1 变量 y_i 决定启用后连续变量 x_i 表示该设备承担的负荷。目标包含三项固定启用成本 f_i·y_i线性可变成本 b_i·x_i以及非线性成本 a_i·x_i²比如热损耗随负荷平方上升。约束是所有设备的总负荷不低于需求 D每台设备负荷不超过容量上限 M_i·y_i。模型写成min Σ (a_i·x_i² b_i·x_i f_i·y_i) s.t. Σ x_i ≥ D 0 ≤ x_i ≤ M_i·y_i y_i ∈ {0,1}数据取a [2.0, 1.0, 0.5]b [1.0, 3.0, 2.0]f [5.0, 4.0, 6.0]M [10, 10, 10]D 8。这个模型规模很小人工都能算出来但用来验证 Bonmin 的调用链路和参数行为足够了。三台设备里第三台非线性系数最小、固定成本最高第二台固定成本最低但线性成本高最优解不是简单“挑成本最低的”需要权衡固定成本和可变成本正好能体现 0-1 变量和非线性耦合的难度。4.2 用 Pyomo 生成模型并调用 bonmin最小可运行代码Pyomo 是连接建模和 Bonmin 最常用的桥梁它负责把模型转换成 AMPL 的 .nl 文件再调用 bonmin 可执行文件求解。先安装 pyomopython -m pip install pyomo然后编写模型import pyomo.environ as pyo m pyo.ConcreteModel() m.I pyo.RangeSet(0, 2) # 连续负荷变量0 到 10 之间 m.x pyo.Var(m.I, bounds(0, 10)) # 0-1 启用变量 m.y pyo.Var(m.I, domainpyo.Binary) m.a pyo.Param(m.I, initialize{0: 2.0, 1: 1.0, 2: 0.5}) m.b pyo.Param(m.I, initialize{0: 1.0, 1: 3.0, 2: 2.0}) m.f pyo.Param(m.I, initialize{0: 5.0, 1: 4.0, 2: 6.0}) def rule_obj(_m): return sum( _m.a[i] * _m.x[i] ** 2 _m.b[i] * _m.x[i] _m.f[i] * _m.y[i] for i in _m.I ) m.obj pyo.Objective(rulerule_obj, sensepyo.minimize) def rule_demand(_m): return sum(_m.x[i] for i in _m.I) 8 m.demand pyo.Constraint(rulerule_demand) # 容量上限绑定到启用变量 def rule_cap(_m, i): return _m.x[i] 10 * _m.y[i] m.cap pyo.Constraint(m.I, rulerule_cap) opt pyo.SolverFactory(bonmin, executable/opt/coin/bin/bonmin) results opt.solve(m, teeTrue) print(results)对这段代码做一些说明。建模层面我故意把 capacity 约束写成x_i 10 * y_i当 y_i0 时强制 x_i0这和模型里的M_i·y_i是对应的Pyomo 会把它转换成 .nl 文件里的线性约束。SolverFactory(bonmin, executable...)中 executable 参数必须给绝对路径因为很多 Linux 环境里 /opt/coin/bin 并不在 PATH 中Pyomo 找不到可执行文件时会报错。teeTrue的意思是把 bonmin 的终端输出直接打印到当前控制台第一次跑建议开着能看到搜索树和 gap 变化全过程。求解完成后检查变量值for i in m.I: print(fy[{i}] {m.y[i].value:.0f}, x[{i}] {m.x[i].value:.4f}) print(obj , pyo.value(m.obj))4.3 bonmin 命令行高频参数算法、时间限制与最优性 gap用 Pyomo 调用时参数可以通过 solver options 传入直接命令行跑 .nl 文件时参数写成--bonmin.xxx的形式。下面这几个参数是实际项目里最常用的。参数默认行为建议设置--bonmin.algorithm默认 B-Hyb明确知道模型非凸时改 B-BB--bonmin.time_limit默认无限制工程任务设 600~1800 秒--bonmin.allowable_fraction_gap默认 0即必须证明最优业务场景放宽到 0.01~0.05--bonmin.print_level默认适中调试时升高生产保持默认time_limit是最值得养成的习惯。Bonmin 的搜索树在某些模型上会异常庞大不加时间限制可能导致任务挂几天。allowable_fraction_gap决定提前终止的条件比如设 0.05表示上下界相对差距小于 5% 时就算收敛工程上这个精度已经足够能省大量时间。直接命令行调用示例/opt/coin/bin/bonmin --bonmin.algorithm B-OA --bonmin.time_limit 300 model.nl4.4 从求解日志里确认结果可信看节点数、gap 和终止状态Bonmin 的日志由三部分拼接而成开头是 Ipopt 的版本声明和 NLP 求解信息中间是 Cbc 为主的分支搜索树信息结尾是 Bonmin 自己给出的终止状态行。第一次跑的人容易被大量中间输出吓到实际只需要盯三个点。第一看根节点 NLP 求解是否成功。日志里会出现Number of Iterations和Optimal这样的关键字如果根节点 NLP 都不可行整个模型可能本身无解。第二看搜索树有没有动起来。Cbc 打印的行里有节点编号和当前上界如果跑了很久节点数一直在涨但上界纹丝不动说明下界质量差要考虑换算法或检查凸性。第三看最后一行终止状态。Optimal表示已经证明最优Feasible表示给了可行解但没证明最优Time Limit表示触达时限这几个状态的差别决定了结果能否被业务直接采信。5. Bonmin 安装与调用避坑笔记从编译失败到结果不合理5.1 编译时提示找不到 ASL 或 ThirdParty 依赖为空现象拿到 Bonmin-master 源码包直接./configure或者make很快停在checking for ASL... no或者报找不到asl.h、libasl。原因Bonmin 读取 .nl 依赖 AMPL 求解器库 ASL。源码包里 ThirdParty/ASL 目录没有实际内容因为从 GitHub 网页按钮下载的 zip 包不包含 git 子模块内容这也是“源码包齐全却编不过”的最常见原因。解决改用 coinbrew 的 fetch 拉取全部依赖。不用 coinbrew 的话在仓库根目录手动补拉子模块git submodule update --init --recursive执行完后确认 ThirdParty/ASL 下面出现了src/和configure这类真实文件再重新 configure。血泪经验不要跳过这步直接 make报错只会更混乱。5.2 没有 gfortranMUMPS 编译到一半失败现象build 命令进行到 ThirdParty/MUMPS 或 Ipopt 阶段时输出大段 Fortran 编译报错比如某种Error指向 mumps 源文件或者直接提示找不到gfortran可执行文件。原因Bonmin 底层的 Ipopt 在解稀疏线性方程组时默认调用 MUMPS而 MUMPS 的源码是用 Fortran 写的。系统里装了 gcc 不等于装了 gfortran很多精简服务器镜像默认不包含 Fortran 编译器。解决sudo apt-get install gfortran装完确认gfortran --version能输出版本然后清理 build 缓存重新跑 coinbrew build。如果继续失败把错误信息里提到的具体文件贴到搜索框查多半是 gfortran 版本过旧或与编译器头文件不匹配。5.3 Pyomo 调用 bonmin 报错但命令行直接跑正常现象已经在命令行验证/opt/coin/bin/bonmin --help、--version都正常但 Python 里SolverFactory(bonmin).solve()抛Solver (bonmin) did not exit normally或ApplicationError。原因常见原因有两个。一是 executable 参数没给Pyomo 在 PATH 里找不到 bonmin二是 bonmin 的共享库比如 libipopt.so 在运行时不在LD_LIBRARY_PATH中命令行能跑是因为某个 shell 环境变量曾经 export 过Python 子进程没有继承。解决Pyomo 侧给绝对路径opt pyo.SolverFactory(bonmin, executable/opt/coin/bin/bonmin)同时把库目录写进环境export LD_LIBRARY_PATH/opt/coin/lib:$LD_LIBRARY_PATH再用ldd /opt/coin/bin/bonmin | grep not found确认没有未解析的库。还有一种隐蔽情况本机装了 Anaconda 或 pyenv 这类多 Python 环境Pyomo 的建模组件加载到了不同版本的 libstdc表现为进程崩溃而不是正常报错。这种情况建议先切回系统自带 Python 跑通最小模型再考虑环境隔离问题。5.4 求解结果里整数变量出现分数值现象Bonmin 正常结束后检查 y[i].value 得到 0.7、0.45 这类连续值模型却宣称 Optimal。原因大概率不是 Bonmin 的问题而是建模层把求解器配成了 Ipopt或者 .nl 文件生成时变量类型被写成了连续变量。Bonmin 的分支定界机制理论上不会产生分数整数解。解决先打印 SolverFactory 对象确认底层求解器叫 bonmin而不是 ipopt再检查 Pyomo 模型里 y 变量确实声明了domainpyo.Binary最后把生成的 .nl 文件单独导出打开看变量类型段二进制变量应该标记为 b 类型而不是 c 类型。如果模型是别人给的还要确认中间转换环节有没有把整数约束丢掉。5.5 小模型却长时间不收敛分支节点数暴涨现象模型总共只有 3 个 0-1 变量跑 10 分钟不停日志里节点数每过几秒翻倍上界和下界差距一直不闭合。原因最常见的是模型非凸B-OA 或 B-Hyb 的线性化平面切掉了可行域里的最优解导致搜索树一直找不到真正好的整数解其次是模型里数值量级差距过大比如同时出现 1e-9 和 1e6Ipopt 的 NLP 求解数值困难每解释出来的下界都很差。解决第一步改--bonmin.algorithm B-BB跑 120 秒看节点收敛率是否改善第二步做变量缩放把目标、约束的量级统一到 1100 范围第三步对非凸表达式做凸松弛或者分段线性近似。业务模型强烈建议同时设置time_limit和allowable_fraction_gap让求解器在可接受的次优程度下交卷而不是死磕最优性证明。6. 让 Bonmin 结果可信三算法横向对比与枚举法验证6.1 同一模型用三种算法各跑一遍用日志横向对比拿到一个新模型第一步不是直接上生产而是用三种算法各跑一次看收敛行为。这个习惯能快速暴露模型的凸性和数值问题for algo in B-BB B-OA B-Hyb; do echo $algo /opt/coin/bin/bonmin --bonmin.algorithm $algo --bonmin.time_limit 60 model.nl done对比时重点看两个数搜索树节点数和最终 gap。B-OA 在凸问题上节点数通常最少B-BB 节点数最多但下界更稳B-Hyb 应该介于两者之间。如果某个算法在 60 秒内没有产生任何像样的上界说明这个模型对这个算法的适配性差后续生产就避开它。6.2 小规模模型的最优性验证Python 枚举后与 Bonmin 对答案对于只有 3 个 0-1 变量的模型有一个更硬核的验证手段枚举全部 2³ 种整数组合对每种组合固定 y 后求解一个连续 NLP取全部组合中的最优值作为参考答案。固定 y 后的子问题规模很小用 scipy 的 SLSQP 就能解。import itertools from scipy.optimize import minimize A [2.0, 1.0, 0.5] B [1.0, 3.0, 2.0] F [5.0, 4.0, 6.0] M [10, 10, 10] best float(inf) best_y None best_x None for y in itertools.product([0, 1], repeat3): if sum(y) 0: continue bounds [(0, M[i] * y[i]) for i in range(3)] def fun(x): return sum(A[i] * x[i] ** 2 B[i] * x[i] F[i] * y[i] for i in range(3)) def cons(x): return sum(x[i] for i in range(3)) - 8 res minimize(fun, x0[4, 4, 4], boundsbounds, constraints{type: ineq, fun: cons}, methodSLSQP) if res.success and res.fun best: best res.fun best_y, best_x y, res.x print(enumerate best:, best, best_y, best_x)这个枚举结果和 Bonmin 的目标值对比如果两者一致说明求解链路和模型转换都没有问题。注意枚举时每个 y 固定后的 NLP 是凸问题SLSQP 解出的就是该子问题的最优解因此 8 个组合里的最小值就是整个模型的参考最优值。这个方法只适合 n 很小的验证场景但作为“对答案”的基准已经足够。我现在拿到一个新的 MINLP 模型习惯是先跑一遍 Pyomo 加 Bonmin 的最小脚本再用三算法对比和枚举法交叉验证确认结果一致后才敢把求解器接进业务流程。这套流程帮我过滤掉了八成模型层面的低级问题。希望帮到你。本文还有配套的精品资源点击获取