moonopt

    Optimization kernels for MoonBit: MPS/LP model interop, a sparse revised simplex with presolve and a dual simplex warm start, branch and bound for models with integer variables, and independently checkable optimality, infeasibility and unboundedness certificates.

    optimization
    linear-programming
    mixed-integer-programming
    simplex
    mps
    Download zip
    Author
    Version
    0.6.0
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    79

    #moonopt

    check

    MoonBit 的线性与整数规划求解器:读标准模型文件,用稀疏修正单纯形求解,对整数模型做分支定界, 并对每个结论给出一份可被第三方独立校验的证书。纯 MoonBit 实现,无 FFI 依赖。

    • 模型输入:MPS(free / fixed)、CPLEX LP
    • 线性求解:稀疏 CSC + 稀疏 LU 基分解的修正单纯形,增益定价,Harris 两遍比值检验,有界变量枢轴, 对偶单纯形热启动,presolve / postsolve
    • 整数求解:best-bound 分支定界,节点热启动,根割(混合整数舍入,附推导证明), 取整 / 下潜 / 可行性泵三种原始启发式,0/1 集覆盖模型上的列支配归约(默认开,精确变换)
    • 证书:最优性、Farkas 不可行性、无界射线,可序列化为 JSON;verify 包独立复核,不依赖 simplex
    • 目标平台:native / wasm / wasm-gc / js

    已发布版本 0.6.0 在 mooncakes.io。 仓库当前 240 个测试在 native / wasm-gc / js 三个目标上全绿;CI 在 Linux / macOS / Windows 上跑检查、构建与测试。

    #安装

    moon add Freon793/moonopt

    需要 moonc v0.10.16 或更新版本(moon version --all),CI 的每一条日志里都记着它实际用的那一套。 源码按当前工具链的写法书写:类型标注用 Int32,默认参数用 name~(moon fmt 会把旧的 Int / name? 改写过来)——所以更低版本只会得到一棵需要改写的树:v0.10.14 直接报 The type Int32 is undefined, v0.10.15 能构建,但它的 moon fmt / moon info 会把接口文件里的 Int32 写回 Int,于是这两个命令会 显示一份 .mbti 差异。.mbti 由 v0.10.16 生成。

    #快速开始

    /// max 5x + 4y s.t. 6x + 4y <= 24, x + 2y <= 6, x, y >= 0 -> 21 at (3, 1.5)
    fn demo() -> Unit {
    let m = @model.Model::new(@model.Sense::Maximize)
    let x = m.add_var("x")
    let y = m.add_var("y")
    m.set_objective([(x, 5.0), (y, 4.0)])
    m.add_named_constraint([(x, 6.0), (y, 4.0)], @model.Rel::LessEqual, 24.0, "machine_hours")
    m.add_named_constraint([(x, 1.0), (y, 2.0)], @model.Rel::LessEqual, 6.0, "labour_hours")
    let sol = @moonopt.solve(m)
    match sol.status {
    @moonopt.SolveStatus::Optimal => println("objective = " + sol.objective.to_string())
    _ => println("not solved: " + sol.message)
    }
    }

    用到两个包:Freon793/moonopt(求解入口)与 Freon793/moonopt/model(模型构造)。 整数变量用 m.add_var("n", lb=0.0, ub=10.0, is_int=true) 声明,solve 会自动走分支定界。

    三个完整可运行的例子:

    moon run examples/production_plan # 两产品生产计划(最优 21 at (3, 1.5)) moon run examples/transportation # 产销平衡运输问题(全等式约束,走 Phase I;最优 11) moon run examples/schedule_bound # 手上已有一份排班表时它值多少:LP 界 7.5、最优 11、这份方案 12 moon run cmd/main # 打印示例模型与一个"当前不支持"的诚实案例

    示例里 21.00000000002238 这样的末位偏差来自退化扰动(默认 1e-12):求解时给右端项加了一个远小于 可行性容差的确定性微扰来打破退化平局,而所有自检都按未扰动的右端项测量,所以解对调用方写下的模型 仍然可行。要打印成定点格式请自行格式化,示例直接输出原始值。

    #命令行

    cmd/parse 是一个模型巡检 / 求解 / 校验 / 格式转换 / 批量基准工具。

    moon run cmd/parse -- <file.mps> # 巡检:格式、规模、校验结论 moon run cmd/parse -- solve <file.mps> --relax --presolve # 线性求解(默认走 presolve) moon run cmd/parse -- solve <file.mps> --mip --max-nodes 2000 # 分支定界,每个松弛过 verify moon run cmd/parse -- verify <file.mps> --relax # 求解并用独立校验器复核证书 moon run cmd/parse -- verify <file.mps> --certificate cert.json # 只校验证书,不重解 moon run cmd/parse -- fmt <file.mps> --format lp -o out.lp # 读→写 moon run cmd/parse -- bench --manifest instances.txt --mip --max-nodes 300

    首参数是 parse / solve / verify / fmt / bench 之一时按子命令解释,否则按旗标形式解释, 两者都支持。加 --json 得到机器可读输出:{"tool","command","files":[...],"summary":{...}}, 没跑出来的字段不出现(而不是填 0),数字用最短往返表示、不取整。

    退出码:解析失败、证书被拒、报告的点没通过复核都是非零;"模型超出内核受理范围"不算失败 (在 JSON 里是 refused),因为它是一个明确的结论而不是错误。

    #支持的模型

    支持暂不支持
    min / max;≤ / ≥ / = 任意混合(含负右端项)MPS 的 SC / SI 半连续界、完整的 SOS / MARKER 语义
    任意有限上下界、自由变量行支配检测、更强的整体 presolve 归约(现有:空行空列、冗余行、singleton 转界、隐式界收紧、对偶固定、固定变量消元 + 解还原)
    三种定价规则:Dantzig、实测增益(默认)、Devex(SimplexOptions::with_pricing,CLI --pricing)精确 steepest-edge(只做 Devex 近似)
    整数与 0-1 变量(分支定界;整点松弛上的系数强化、0/1 集覆盖模型上的列支配(MipOptions::column_dominance,默认开,删列后把解映回原变量)、cutoff 收紧节点盒子;松弛无界且点 / 射线(或其最小整数倍)整数时证 Unbounded)节点上的割;点本身是分数时的整数可行点(仍报 UnboundedRelaxation)
    证书复核(最优性 / Farkas / 无界射线)化简模型的乘子回映(证书只对内核收到的模型成立,故校验路径不化简)
    内核行数上限 200 000(稀疏 LU,内存 O(nnz + fill))非线性 / 半定规划、并行、网络单纯形专用路径、Python / JS 绑定

    超出能力边界的调用一律返回 NotSolved 并带原因,不会返回一个可疑解。详细的状态口径见 docs/api.md。

    #什么时候它不是合适的选择

    这一节写给正在选求解器的人。每条都是本项目的实测边界,不是保守估计:

    • 规模:内核行数上限 200 000(稀疏 LU,内存 O(nnz + fill)),超过就返回 TooLarge。清单里 3 个实例抬到 --max-rows 20000 仍解不完——core2536-691 与 dano3_3 卡在 Phase I 的枢轴成本、 2club200v15p5scn 是 17 013 行的规模本身;mod011 是在等式 doubleton 代入把它降到 2 402 行之后 才解出来的。"几千行 + 系数跨若干数量级"这一带,先假设它会失败。
    • 高度退化的实例:fast0507(507 条覆盖行、63 009 个 0/1 变量)20 000 个枢轴没有收敛,60 000 到 139.78、150 000 到 133.18 —— 还在降,但每千枢轴的增益已从 0.61 掉到 0.073。
    • 整数规划要的是"证明":宽口径(32 个实例、600 个节点)证到 5 个最优,小规模(10 个、30 000 个 节点)证到 6 个;其余返回 node-limit 并带上当前解与开界——那是诚实的答案,不是最优性证明。 界本身就贴 LP 松弛的那一类,加预算也推不动(noswot 从 30 000 翻到 60 000 个节点,界一动没动)。
    • 想靠割平面抬界:只有单行的混合整数舍入割,且只在根节点做。选择规则(五条)、割族(多行 MIR 聚合、二值 cover、覆盖型 cover、flow cover 的结构前提)、节点割都已实测并关闭。反过来, markshare1 / markshare2 是纯等式模型(没有 ≤/≥ 行对)、noswot 的"需求行"其实是 job-shop 的时序约束——这两类的界不会被覆盖类割抬高,换哪一族都一样。
    • 需要证书被外部工具读:证书是 JSON,但只对内核收到的模型成立(校验路径不化简),化简模型的 乘子不会回映到原模型;换成有理数证书(例如 VIPR)需要另一套算术子系统,本项目没做。
    • 需要一个整数可行点、而松弛点本身是分数:整数无界性的证明要求"点与改善射线在整数变量上都取整数", 点本身分数时只报 UnboundedRelaxation(松弛无界、模型未知),找可行点仍归搜索。
    • 以下功能不在计划里:非线性 / 半定规划、并行、网络单纯形专用路径、Python / JS 绑定、MPS 的 SC / SI 半连续界与完整 SOS / MARKER 语义、行 / 列的支配检测、精确 steepest-edge 定价。

    #真实数据与基准

    bench/ 用公开实例(MIPLIB 2017,仓库不分发数据,只提供下载脚本与清单)对拍, 产出四份报告:

    报告口径结果
    bench/parse-report.md全清单解析32 个实例全部解析成功
    bench/solve-report.md32 个实例、2500 行上限、20000 枢轴上限、presolve28 个求到最优、3 个超行数上限跳过(2club200v15p5scn、core2536-691、dano3_3)、1 个到枢轴上限(fast0507:被挡住的三个抬到 20000 行后实测仍解不完;撞上限的那个抬到 150 000 枢轴仍未收敛且增益超线性衰减,见 bench/README.md)
    bench/mip-report.md32 个实例、600 节点5 个证到最优、10 个到节点预算、17 个跳过
    bench/mip-report-small.md10 个实例、30000 节点6 个证到公开已知最优值、4 个到节点预算

    上限是结果的一部分:报告头部逐字记录行数上限、枢轴上限与节点预算,不同上限下的计数不能互相比较。 bench/report.md 是索引,同时判定每份报告描述的还是不是当前代码里的那份。

    powershell -NoProfile -ExecutionPolicy Bypass -File bench/fetch-instances.ps1 # 下载实例 powershell -NoProfile -ExecutionPolicy Bypass -File bench/report.ps1 # 四份报告是否仍被当前代码支持

    #项目结构

    moonopt.mbt 公开入口:solve / solve_with、SolveStatus、Solution、SolveOptions core/ 数值与稀疏基础设施(容差比较、补偿求和、CSC 稀疏矩阵) model/ 模型层(变量、线性表达式、约束、目标、模型校验) format/ MPS 与 LP 读写、解析错误定位 oracle/ 参考实现:稠密两阶段单纯形(差分测试的对照基准,不是交付求解器) simplex/ 稀疏修正单纯形内核、稀疏 LU、对偶单纯形热启动、证书生产端自检 presolve/ 模型化简与解还原 verify/ 独立校验器:最优性、Farkas、无界射线、割的再推导(不依赖 simplex) mip/ 分支定界、根割、原始启发式 cmd/main/ 演示 CLI cmd/parse/ 模型巡检与求解 CLI examples/ 可运行示例 bench/ 数据政策、下载与报告脚本、报告 docs/ 设计、算法、API 契约、路线图、生态调研、验收对照

    #文档

    文件内容
    docs/design.md架构:目标与非目标、包边界与依赖规则、数据表示、证书约定
    docs/algorithms.md实现里真正在跑的算法,每节附一条可复核的实测数字
    docs/api.md公开 API 契约:每个包承诺什么、什么情况下返回哪个状态
    docs/roadmap.md里程碑与完成标准、下一步方向、明确不做的事
    docs/prior-art.md生态调研:生态内有无同类实现、可借鉴的公开标准、许可证红线
    bench/README.md基准数据政策、实例量级、四份报告的复现步骤与口径
    CONTRIBUTING.md本地环境、提交前必须全绿的检查、代码规范
    CHANGELOG.md逐版变化
    docs/history.md开发过程中的实测记录(归档,含被否决的方案与理由)

    #开发

    moon check --deny-warn moon test --deny-warn moon fmt && git diff --exit-code moon info && git diff --exit-code # .mbti 是接口合同,必须随代码提交

    库包(core / model / format / oracle / simplex / presolve / verify / mip / 根包) 不依赖任何第三方包;只有 cmd/parse 依赖官方 moonbitlang/x 的 fs 与 sys。

    #许可与数据

    • 本项目以 Apache-2.0 发布,见 LICENSE。
    • 算法实现基于公开文献,代码为本项目原创,未移植任何第三方实现。
    • 基准实例(MIPLIB 2017)是第三方数据,不由本仓库再分发:仓库里只有下载脚本与本项目自己写的清单, .moonignore 保证它们不进发布归档。细节见 bench/README.md。

    Solution

    pub struct Solution {
    status : SolveStatus
    values : Array[Double]
    objective : Double
    iterations : Int
    nodes : Int
    bound : Double
    gap : Double
    message : String
    } derive(
    Debug
    )

    What a solve call produced.

    values is indexed by variable index. It is empty unless the status is Optimal or NodeLimit; for NodeLimit it is a feasible integer point that is not proven optimal. message carries the reason otherwise, and also the kernel's or the search's own note when a run needed its recovery attempt or reached no verdict on a relaxation.

    SolveOptions

    pub(all) struct SolveOptions {
    max_iterations : Int
    presolve : Bool
    max_nodes : Int
    cut_rounds : Int
    } derive(
    Debug
    )

    Tunables for a solve call.

    pub(all) so callers can write { max_iterations: 1000, presolve: false } directly; SolveOptions::new is the readable alternative.

    SolveOptions::default

    fn SolveOptions::default() -> SolveOptions

    Default solve options: a generous iteration budget and node budget, presolve on, cuts on.

    SolveOptions::new

    fn SolveOptions::new(max_iterations : Int) -> SolveOptions

    Solve options with an explicit iteration cap, keeping presolve on.

    SolveOptions::with_max_nodes

    fn SolveOptions::with_max_nodes(max_nodes : Int) -> SolveOptions

    Solve options with an explicit node budget, for a model with integer variables.

    SolveStatus

    pub(all) enum SolveStatus {
    Optimal
    Infeasible
    Unbounded
    NodeLimit
    NotSolved
    } derive(Eq,
    Debug
    )

    Outcome of a solve call.

    relaxation

    The same model with every integrality flag cleared — its LP relaxation.

    This package answers a model with integer variables with the branch and bound (mip), on purpose: a relaxation is not an answer to an integer model. A caller who wants the bound instead — the number a plan they already have is measured against — asks for that explicitly, and this is the question: the same variables in the same order, the same rows, the same objective, no integrality. What comes back is an ordinary linear model, so it goes through Model::solve like any other, reduction and independent verification included, and bound/gap mean what they mean there (a linear solve is its own bound).

    The variables keep their indices, so a point the caller already holds is still a point of the same space and can be read back on the relaxation: compare its objective_value with the relaxation's optimum to get the gap the plan is worth, and check max_row_violation/max_bound_violation on it if the plan is not known to be feasible. examples/schedule_bound is that whole workflow in one file.

    solve

    Solves model with default options.

    solve_with

    fn solve_with(model :
    Model
    , options : SolveOptions) -> Solution

    Solves model and returns a Solution describing what happened.

    The work happens in the simplex kernel; this function validates the model, reduces it, maps the kernel's status onto the public one, and makes sure that a question the kernel cannot answer comes back as NotSolved with a reason rather than as a plausible looking answer.

    The reduction is never trusted on its own account. When presolve proves the model infeasible or unbounded, that verdict is returned without a kernel run; when it returns a reduced model, the kernel's solution is reconstructed and then measured against the original rows, bounds and objective, and a reconstruction that does not verify turns into NotSolved rather than an answer.

    Integer models are the one class the reduction is not offered: the reduction must not be the reason an integer model gets an answer, because that answer would depend on whether the reduction happened to fix every integer variable — and that is exactly where a fractional fixed value would slip through. Such a model goes straight to the branch and bound, on the model as written.