
简介Bonmin-master 是开源混合整数非线性规划MINLP求解器 Bonmin 的主分支源码包面向需要求解含整数变量与非线性函数优化问题的科研人员和工程师。压缩包共含 300 个文件以 98 个 C 源文件与 91 个头文件为核心另有 25 个.in 配置模板、19 个 TeX 文档、17 个 Automake 脚本及 configure、Makefile 等构建支撑整体约 950KB目录结构清晰便于直接编译或定位相关实现。目前已有 610 人学习下载。通过这份代码读者可以获取 Bonmin 的完整算法实现、依赖说明与编译配置模板在自有环境中搭建基于 LP/NLP 的分支定界求解流程并据此进行 MINLP 问题的建模、实验或二次开发。1. 拿到 Bonmin-master 源码包以后混合整数优化里的 MINLP 求解器到底能解决什么Bonmin-master 是 Bonmin 求解器在 GitHub 上的开发主线源码包Bonmin 解决的是混合整数非线性规划MINLP模型里同时存在 0-1 或整数变量和非线性函数。典型的例子是设备选型离散叠加操作参数连续、非线性的能耗或产量方程电力调度里的机组启停离散叠加交流潮流非线性这些都超出 Cplex、Gurobi 能直接处理的范畴。你需要知道的是Bonmin 本身不是一个建模语言它读 AMPL 的 .nl 格式也提供 C 接口最常见的用法是通过 Pyomo、AMPL 或 GAMS 建模后交给它求解。下面直接以源码包为中心走一遍“编译安装、建第一个 MINLP、调参数、排坑、验证最优性”的完整路径适合手里有 MINLP 模型想尽快跑通的人。2. 源码编译 Bonmin先理清依赖再 configure一条可复现的安装路径2.1 为什么第一反应 pip install 会落空Bonmin 的依赖结构搜索记录里和 Bonmin 绑在一起的高频词是 install。很多人的第一反应是pip install bonmin或者像 Maven 里敲clean install一样指望一个包管理器把所有事搞定。但 Bonmin 不是一个 Python 包也不是一个能通吃所有 Linux 发行版的预编译二进制。它的形态是 COIN-OR 传统的源码包加 configure/make编译前必须把依赖链理清楚否则 configure 会在早期直接报错退场。Bonmin 的依赖结构不算复杂但每层都绕不开。核心依赖大致是这几块依赖库作用缺了会怎样ASL读取 AMPL 的 .nl 模型文件输出 .solconfigure 找不到 ASL 头文件无法生成 bonmin 可执行文件Cbc提供混合整数分支定界框架离散变量搜索部分没有着落Clp作为 LP 子求解器处理分支节点的松弛问题编译链断裂CoinUtils参数解析、文件 IO 等公共工具几乎所有 COIN-OR 项目都依赖它Osi求解器抽象接口层无法把 MIP 问题传给具体的 LP/MIP 求解器Ipopt负责连续非线性子问题求解非线性约束和连续变量直接没处解Bonmin 的工作方式可以理解为外层离散搜索交给 Cbc 的分支框架或者用外近似/割平面生成器B-OA、B-Hyb每一层的 NLP 子问题全部交给 Ipopt。它自己不直接做连续非线性求解所以这些依赖缺一个就过不去。这也是为什么源码包下载后第一件事不是./configure而是先检查 ThirdParty 目录。master 分支还有另一个隐藏坑源码包里的 ThirdParty 子模块ASL、Mumps 等在 GitHub 上是以 git submodule 形式挂进来的。如果你用的是页面上的 Download ZIP 下载子模块目录会是空的。打开 ThirdParty 目录看一眼里面只有 README 或者什么都没有就说明没拉全。2.2 最小可运行的编译步骤clone、configure、make 三步先说最简单的场景本机已经有一个编译好的 Ipopt并且安装前缀是 /opt/ipopt。这时候编译 Bonmin 只依赖 Cbc、Clp、CoinUtils、Osi 和 ASL用源码树自带的 ThirdParty 就能补齐。命令如下git clone --recursive https://github.com/coin-or/Bonmin.git cd Bonmin ./configure --prefix/opt/coinor --with-ipopt/opt/ipopt make -j $(nproc) make install--recursive是 clone 时就必须带的它会顺带拉取 ThirdParty 下的 git 子模块避免后面 configure 报“找不到 ASL”“找不到 CoinUtils”一类问题。--prefix/opt/coinor把可执行文件、库和头文件装到独立目录后续想卸载时直接删目录不会污染系统路径。--with-ipopt/opt/ipopt告诉 configure 去哪找 Ipopt 的头文件和库如果你的 Ipopt 不是标准安装configure 会找不到它这时可以在选项后追加LDFLAGS-L/opt/ipopt/lib CPPFLAGS-I/opt/ipopt/include/coin来补路径。-j $(nproc)用满 CPU 核数是常规操作Bonmin 编译不慢但依赖多单线程要等不少时间。如果本机没有现成的 Ipopt也不需要先去单独编译一个。Bonmin 源码包里自带 coinbrew 脚本它会按需拉取并编译 Ipopt 和它的 ThirdParty 依赖包括 Mumps。一套完整命令是这样git clone --recursive https://github.com/coin-or/Bonmin.git cd Bonmin ./coinbrew build Bonmin --prefix/opt/coinorcoinbrew 做的事情比手动 configure 更省心先处理 Ipopt然后再回来构建 Bonmin依赖顺序和版本匹配都自动处理好。第一次跑 coinbrew 会花不少时间因为 Ipopt 本身的依赖链也不短过程中需要联网下载 Mumps、ASL 等源码包。如果你习惯在 WSL 里装依赖这时最容易碰到的不是编译错误而是 apt 或者下载慢得像卡死通常是源的问题先把系统软件源换到国内镜像再继续能省下半小时。编译完成后安装目录里会出现 bin/bonmin这就是最终的求解器可执行文件。如果你看到的是Bonmin大写开头或者在 build 目录里找不到多半是 configure 阶段出了岔子回头看 2.1 的依赖项把报错信息里的库名逐个对照。2.3 编译完怎么确认能用三个低成本的验证命令装完之后先别急着跑模型花一分钟确认安装真的可用export PATH/opt/coinor/bin:$PATH export LD_LIBRARY_PATH/opt/coinor/lib:$LD_LIBRARY_PATH which bonmin bonmin -?which bonmin确保 shell 能找到可执行文件。LD_LIBRARY_PATH指向安装前缀下的 lib 目录否则运行时可能报error while loading shared libraries: libbonmin.so: cannot open shared object file。bonmin -?会打印一长串参数说明只要它能正常输出而不是报动态库错误就说明可执行文件、共享库和 ASL 驱动都加载成功了。这两个环境变量建议写进~/.bashrc因为后面用 Pyomo 或者直接命令行调用时如果出现“找不到 bonmin”或者动态库加载失败十次里有八次是这一步漏了。下一步用一个小算例把求解链路完整跑通比任何安装日志都可信。3. 用 Pyomo 建一个真正的 MINLP 模型从建模语言到 Bonmin 求解的完整链路3.1 为什么建模层选 Pyomo.nl 中间格式与 AMPL 驱动Bonmin 的输入格式核心是 AMPL 的 .nl 文件它不直接读取 Python 写的模型。Pyomo 在这条链路里扮演的是建模层和翻译层你用 Python 定义变量、目标、约束Pyomo 把它们编译成 .nl 格式再交给 bonmin 可执行文件去解。这样做的实际好处是模型可以用循环、函数、外部数据源来生成而不是靠手写 AMPL 文本。解完之后Pyomo 还会把 .sol 结果映射回 Python 对象直接就能读取每个变量的值。很多人会问那我直接用 AMPL 写行不行当然可以但 AMPL 是商业软件光是要跑一个几十行的 MINLP 模型还没到求解就卡在许可证上。Pyomo 免费、开源而且对 Bonmin 的支持成熟SolverFactory(bonmin)一行就能接上。另一个常见选择是 GAMS但 GAMS 同样需要商业许可。对于工程验证和论文复现Pyomo 是最快的一条路。3.2 一个工艺选择算例二元变量、连续变量与非线性约束怎么落笔用一个制造场景当例子两条候选工艺路线每条可以选择投资0-1 变量或放弃选定后连续处理量 x 决定产出两条路线共享一份资源同时处理量之间存在非线性耦合约束。这类模型在很多领域都有对应物选设备、选通道、选机组本质都是“离散选择 连续非线性寻优”。import pyomo.environ as pyo model pyo.ConcreteModel() # 0-1 决策是否选择工艺路线 1 / 2 model.y1 pyo.Var(withinpyo.Binary) model.y2 pyo.Var(withinpyo.Binary) # 连续操作变量每条路线的处理量 model.x1 pyo.Var(withinpyo.NonNegativeReals, bounds(0, 8)) model.x2 pyo.Var(withinpyo.NonNegativeReals, bounds(0, 6)) # 目标收益最大化 model.profit pyo.Objective( expr5 * model.x1 * model.y1 4 * model.x2 * model.y2 - 2 * model.y1 - 3 * model.y2, sensepyo.maximize ) model.choose_route pyo.Constraint(exprmodel.y1 model.y2 1) # 大 M 链接约束不选路线时处理量必须为 0 model.link1 pyo.Constraint(exprmodel.x1 8 * model.y1) model.link2 pyo.Constraint(exprmodel.x2 6 * model.y2) # 共享资源约束 model.resource pyo.Constraint(exprmodel.x1 model.x2 10) # 非线性约束处理量之间的工艺耦合 model.tech pyo.Constraint( exprmodel.x1 * (model.x2 1) ** 0.5 model.x2 8 )变量声明的关键在于withinpyo.Binary和withinpyo.NonNegativeReals这决定了 Bonmin 的变量类型。目标里的5 * model.x1 * model.y1是双线性项在 MINLP 里很常见但要注意这种项会让模型变成非凸外近似算法不一定可靠后面调参数那章会专门讲。link1和link2是典型的“大 M”链接约束作用是把离散变量和连续变量绑定y 取 0 时 x 被强制压到 0y 取 1 时 x 放开到容量上限。如果不加这两条模型会出现“不建这条路线却仍然产生处理量”的荒谬解这是 MINLP 建模里最常见的错误之一。model.tech里有根号运算Pyomo 会把这类表达式翻译成 .nl 里的非线性算子Bonmin 再交给 Ipopt 处理。这里特意留了一个值得注意的点这个非线性约束在 x2 接近 0 时仍然光滑不会出现除零问题。如果你的模型里有x/y这种除法y 取 0 时整个约束在数学上无定义Bonmin 和 Ipopt 会给你很难看的失败日志务必先用大 M 约束或边界把分母约束到正数区间。3.3 调用 bonmin 求解并判定结果终止状态与目标值怎么读模型写完接下来就是调用求解器。这一步代码很短但参数和状态判断值得仔细看opt pyo.SolverFactory(bonmin, executable/opt/coinor/bin/bonmin) opt.options[bonmin.algorithm] B-OA opt.options[bonmin.time_limit] 120 results opt.solve(model, teeTrue) print(results.solver.termination_condition) print(pyo.value(model.profit)) print(pyo.value(model.y1), pyo.value(model.y2)) print(pyo.value(model.x1), pyo.value(model.x2))SolverFactory(bonmin)是 Pyomo 里对接 Bonmin 的入口executable参数直接指定刚编译好的可执行文件路径这一步能绕开很多 PATH 配置问题。opt.options里的键值对会作为求解器参数传给 bonminbonmin.algorithm选外近似算法bonmin.time_limit限制 120 秒。teeTrue会把求解日志打到屏幕上第一次跑建议开着能看到分支节点数、NLP 子问题迭代次数和当前目标值。判断结果时会有一个最容易踩的误区termination_condition显示optimal才代表收敛。如果显示feasible说明在时间或节点限制内只找到了可行解还没证明最优显示stopped或maxTimeLimit说明是时间到了被截断。后半句的取值代码pyo.value(model.profit)是求当前目标函数值pyo.value(model.y1)是取变量解出的数值。我一般会把 termination_condition、目标值、每个整数变量的值一起打印出来存进日志文件后面调参时对比才方便。这个算例如果跑通理论上你会看到最优解落在“只选路线 1处理量拉满到 8”的位置目标值 38。如果结果不是这样先回去检查有没有漏了大 M 约束或者求解时间被限得太死。这个模型小到可以心算验证非常适合当 Bonmin 的冒烟测试用例。4. Bonmin 的参数调优四种算法、容忍度与求解规模的取舍4.1 四种算法怎么选B-BB、B-OA、B-Hyb、QP-Diving 的适用场景第一次跑通模型后紧接着就是调参。Bonmin 提供的求解算法有四种选错算法会直接体现为“死活解不动”或者“解出来是错的”。这四种算法的核心差异在怎么处理离散变量和非线性子问题之间的关系。算法参数值搜索策略适合场景B-BB纯分支定界节点松弛直接交给 Ipopt非凸严重的 MINLP外近似容易切错区域时B-OA外近似用线性切平面逼近非线性凸 MINLP约束和目标都相对光滑B-Hyb外近似与分支定界混合默认选择凸与非凸混合模型的折中方案QP-Diving用 QP 松弛引导深度优先 diving时间紧张只求一个高质量可行解时从实际使用者的角度看我的经验是这样的如果你的模型里非线性项都是凸的比如凸二次函数、指数和、Log 约束B-OA 收敛快节点少是首选。如果你的模型里不可避免地出现了双线性项、sin/cos、或者求商B-OA 的外近似切平面可能把可行域切掉一块收敛到假最优解这时候换 B-BB 更稳。B-Hyb 作为默认值大多数情况下表现均衡但遇到强非凸问题同样会卡。QP-Diving 不是用来证明最优的而是用来快速拿到一个可行解当预案比如调度场景里先要一个能执行的方案再慢慢优化。切换算法在 Pyomo 里就是改一行参数。可以先用 B-OA 跑一遍再用 B-BB 跑一遍对比目标值。如果两个目标值差异超过容忍度基本可以断定模型存在非凸性需要回去检查建模或者接受 B-BB 的结果。4.2 收敛判据与 Ipopt 子问题参数日志里哪些行值得盯Bonmin 的求解日志会把每一层的 NLP 子问题状态刷出来。初学者常犯的错误是看最后一行“Optimal”就当万事大吉但其实中间过程里藏着大量信息。日志里反复出现EXIT: Restoration Phase Failed或Too many infeasible NLP iterates说明子问题求解器在收敛阶段翻了车。这里要理解 Bonmin 的双层结构外层是离散搜索内层是 Ipopt 解 NLP。Ipopt 是连续非线性优化器它需要目标函数和约束的一阶导数当模型数值条件差比如量级差十几个数量级或者初值偏到奇异点Ipopt 可能连一个可行子问题都解不出来。这时先别急着喊 Bonmin 不行要看看是不是 Ipopt 收敛条件设置太苛刻。两类参数值得调一类是整数层面的判定容差常见做法是设bonmin.integer_tolerance1e-6它控制变量值离整数多近时判定为整数默认通常已经够用但如果你的模型数值量级很大可能需要放宽。另一类是 Ipopt 自身的收敛容差通过ipopt.tol传入opt.options[bonmin.integer_tolerance] 1e-6 opt.options[ipopt.tol] 1e-7 opt.options[ipopt.max_iter] 500ipopt.tol是 NLP 子问题的 KKT 收敛容忍度默认在 1e-8 量级如果你的模型本身精度要求不高放到 1e-6 会让整个求解快很多我经常对大型调度模型这么做。ipopt.max_iter限制子问题最大迭代次数防止单个 NLP 子问题卡住不返回。这些参数的具体名称可以在命令行执行bonmin -?查看帮助输出不同版本命名会略有差异但前缀规则是一致的带bonmin.的归外层带ipopt.的透传给子求解器。4.3 时间限制、可行解数量与容量扩展把参数写进求解配置工程场景里很少有模型能无限时求解。Bonmin 支持直接限制求解时间和输出解的数量这两个参数搭配起来很实用opt.options[bonmin.time_limit] 300 opt.options[bonmin.solution_limit] 5time_limit的单位是秒300 表示最多跑五分钟。solution_limit的意思是找到 5 个可行解就提前停止。这在工业场景里很有用不追求全局最优的情况下先拿 5 个可行解做对比选一个工程上最容易实现的方案比傻等最优解划算得多。还有一组容易被忽略的容量参数是输出等级。调试时可以把日志打到最详细看每个节点在做什么生产环境则要压到只输出警告否则几十万节点的日志文件能把你磁盘塞满。常见做法是设bonmin.print_level2数字越大输出越详细默认值一般在中间档位。我的习惯是调试用详细日志正式批量求解只保留 termination_condition 和目标值。关于“什么情况别用 Bonmin”也值得在这里说清楚如果模型里没有整数变量直接用 Ipopt 就够了Bonmin 的外层框架是纯开销如果模型可以线性化优先线性化后用 Cbc 或 Cplex如果你的 MINLP 是二次约束二次目标QCQP先评估是否能用 Gurobi 或 Cplex 的二次求解能力。Bonmin 的适用范围是那些确实处理不了离散与非线性叠加的问题用在对的地方性价比很高。5. Bonmin 安装与求解的五个常见坑从编译失败到求解卡死的排查记录5.1 坑一configure 报找不到 ASL 或 ThirdParty 目录为空现象执行./configure中途报错关键词是Cannot find ASL或者Cannot find CoinUtils看起来像系统缺库。原因源码包里的 ThirdParty 是 git submodule直接 Download ZIP 下载时子模块目录是空的。configure 检查源码树里的 ASL、Blas、Mumps 时找不到于是中止。解决如果已经用 git clone 拉过仓库但没有加--recursive执行git submodule update --init --recursive补拉子模块然后重新 configure。如果是从 ZIP 解压的最简单的做法是删掉重来用git clone --recursive https://github.com/coin-or/Bonmin.git重新拉一份。这个坑之所以高频是因为 GitHub 页面默认提供的 ZIP 包太有迷惑性让人以为所有文件都在里面。5.2 坑二运行时报找不到共享库 libbonmin.so现象编译和安装都成功了which bonmin也能找到可执行文件但一运行就报error while loading shared libraries: libbonmin.so: cannot open shared object file。原因Bonmin 安装到了非系统目录比如 /opt/coinor/lib而 Linux 的动态库加载器默认不搜索这个路径。解决在运行前导出LD_LIBRARY_PATH/opt/coinor/lib:$LD_LIBRARY_PATH并写入~/.bashrc。如果用了ldconfig方式需要把 /opt/coinor/lib 写进 /etc/ld.so.conf.d/ 下的配置文件再执行ldconfig。对单用户使用场景export 最简单可靠对服务器部署建议用 ldconfig 方式避免每次开新 shell 都要重新设变量。5.3 坑三Pyomo 说找不到 bonmin 可执行文件现象Python 代码里SolverFactory(bonmin)直接抛出错误提示找不到可执行文件或者返回 code 127。原因Pyomo 默认在 PATH 里搜索 bonmin但你的安装目录还没加进 PATH。Python 进程和 shell 不一样不会自动读你~/.bashrc里 add 到 PATH 的那个目录。解决第一种做法是显式指定路径SolverFactory(bonmin, executable/opt/coinor/bin/bonmin)一劳永逸代码可复用性也好。第二种做法是确认当前 shell 的 PATH 真的包含了 Bonmin 的 bin 目录执行echo $PATH验证。我个人的习惯是选择第一种因为脚本换机器时只需要改一处路径配置不依赖系统环境。5.4 坑四求解长时间卡死节点数不增长现象跑大型模型时日志里节点数长时间停在同一个数字或者反复出现 NLP 子问题失败的提示CPU 占用却很高。原因这是 MINLP 求解最典型的症状。外层分支定界每走一步都要解一个 NLP 子问题如果子问题在指定初始点周围找不到可行解节点就不会继续展开。常见的背后原因是非凸约束让外近似切错区域或者连续变量的边界太宽Ipopt 在数值上迷失方向。解决第一步换算法把bonmin.algorithm设成 B-BB绕过外近似切平面。第二步给连续变量收窄边界比如原来写bounds(0, 1000)根据物理含义缩到bounds(0, 100)数值范围减小对 Ipopt 的收敛帮助极大。第三步给变量设置初始点model.x1.set_value(50)这类赋值写在 solve 之前让子问题从合理位置出发。这三板斧能解决大部分卡死问题。如果还不收敛回到建模层面看约束是否真的可行。5.5 坑五Ipopt 报 MUMPS is not available 或线性求解器初始化失败现象Bonmin 运行时日志里出现MUMPS is not available或者明确提示Linear solver失败求解直接中止。原因Ipopt 求解 NLP 子问题需要线性代数求解器MUMPS 是默认依赖而它属于 Ipopt 的 ThirdParty 组件。如果你的 Ipopt 是手动单独编译的编译时没拉 MUMPS 就会缺这个能力。解决如果 Ipopt 是用 coinbrew 跟着 Bonmin 一起拉起来编译的通常不会有这个问题因为 coinbrew 默认会编译 MUMPS。如果是手动编译 Ipopt回到 Ipopt 源码目录执行./coinbrew build Ipopt --prefix/opt/ipopt它会自动补充 MUMPS 并重新编译。做完之后把 Bonmin 重新 configure 一遍指向新的 Ipopt 前缀。另外注意如果机器上装了多个版本的 Ipopt用ldd /opt/coinor/bin/bonmin检查它实际链接的是哪个 libipopt避免链接到旧版本。6. 用固定整数再求解验证 Bonmin 结果一个成本最低的最优性检验技巧6.1 为什么需要二次求解验证Bonmin 给出optimal不代表结果可以闭眼信任尤其是非凸问题。整数变量被分支定界确定为某个组合后外近似算法也可能因为切平面偏差锁定在次优区域。与其依赖求解器一句话不如自己做一个低成本交叉验证把整数变量固定为已求出的解整个模型退化为一个连续 NLP再交给 Ipopt 精确求解一次。如果目标值和原来的 MINLP 结果基本一致说明这个整数组合在连续空间里是稳定的如果差异很大说明最初搜索时某个环节出了问题。6.2 脚本实现读解、固定整数、二次求解在第三章模型的基础上继续操作核心代码只有十几行# 保存原始 MINLP 解 obj_minlp pyo.value(model.profit) sol_y1 int(round(pyo.value(model.y1))) sol_y2 int(round(pyo.value(model.y2))) # 固定整数变量 model.y1.fix(sol_y1) model.y2.fix(sol_y2) # 退化为 NLP 后重新求解 opt2 pyo.SolverFactory(bonmin, executable/opt/coinor/bin/bonmin) obj_fixed opt2.solve(model, teeFalse) obj_fixed_nlp pyo.value(model.profit) print(fMINLP 解目标值: {obj_minlp:.6f}) print(f固定整数后 NLP 目标值: {obj_fixed_nlp:.6f}) if abs(obj_fixed_nlp - obj_minlp) 1e-3: print(差异过大怀疑原解未收敛建议改用 B-BB 并收窄边界重解。)fix是 Pyomo 里把变量固定在当前值的标准方法固定之后求解器不再对整数变量做分支决策Bonmin 会直接把问题压缩成连续 NLP 交给 Ipopt。abs判断的阈值我习惯取 1e-3但实际取决于模型数值量级如果你的目标函数值本身只有 0.0001 量级阈值要同步缩小如果目标值以百万计1e-3 的绝对误差可能反而过于苛刻改用相对误差更合理。6.3 记录求解日志与参数复现的习惯做完验证还有一件事值得养成习惯把每一轮求解的算法选项、时间限制、termination_condition、两个目标值同步记录成文件。我见过太多“上次明明解出 42.7重跑变成 42.6”的对账事故最后发现是参数没记全某个 tolerances 或初始点不一样。技术上这不算难题但很耗人。我的做法是在每次求解前把opt.options的字典全部打印出来存档求解结束后把目标值、整数变量解、终止状态追加到同一行。回头对比时先看参数差在哪再看目标值差在哪。这套习惯帮我省了不少排查时间。如果你打算把 Bonmin 用在正经项目里建议从第一天就建立这个记录习惯。希望帮到你。本文还有配套的精品资源点击获取