# 基准数据与报告

## 数据政策（重要）

**本仓库不再分发第三方测试数据集。** 只提供下载脚本、清单与报告；数据落在 `bench/data/`，该目录已被
`.gitignore` 忽略。

| 数据集 | 来源 | 获取方式 |
| --- | --- | --- |
| MIPLIB 2017 | <https://miplib.zib.de> | `bench/fetch-instances.ps1` 下载 `.mps.gz` 并就地 gzip 解压为纯 MPS |

### 为什么不用 Netlib LP 测试集

netlib 的 `lp/data/` **不是纯 MPS**：文件由一行 `NAME`、一行维度头，随后是它自己的压缩记录组成
（netlib 的 emps 容器）。在实现该容器的解压器之前，这些文件无法直接读取，因此解析报告改用
MIPLIB 2017 —— 它提供同样性质的工业实例，但以纯 MPS（gzip 压缩）发布。

如果将来要接入 Netlib，正确做法是先实现/移植 emps 解压，而不是把压缩数据喂给 MPS 读取器。

## 脚本

| 脚本 | 作用 |
| --- | --- |
| `fetch-instances.ps1` | 下载实例到 `bench/data/instances/`，生成 `manifest.txt`（相对路径，可移植） |
| `report-parse.ps1` | 通过 `moon run cmd/parse -- --manifest ...` 解析全部实例，生成 `parse-report.md` |
| `report-solve.ps1` | 加 `--solve --relax --max-rows N` 求解，生成 `solve-report.md` |
| `report-mip.ps1` | 加 `--mip --max-nodes N` 做分支定界，生成 `mip-report.md`（可换 `-Manifest` / `-Output` / `-CutRounds`：小规模满预算那一份就是这么生成的） |
| `check-relaxation-bounds.ps1` | 把求解报告里的目标值与 MIPLIB 官方最优值表对拍 |
| `check-mip-objectives.ps1` | 把分支定界报告里每个 `optimal` 与官方最优值**取等**对拍，并断言 `verified == nodes` |
| `report.ps1` | 生成 **`report.md`**：四份报告的索引（每份的范围、关键数字、生成时的提交）+ **一键复现命令** + "这份报告描述的代码是否仍是仓库里那份"（按报告自己记的提交与它依赖的源路径做差分判定） |

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File bench/fetch-instances.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File bench/report-parse.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File bench/report-solve.ps1 -Relax -MaxRows 2500 -MaxIterations 20000 -Presolve
powershell -NoProfile -ExecutionPolicy Bypass -File bench/check-relaxation-bounds.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File bench/report-mip.ps1 -MaxRows 300 -MaxNodes 600 -MaxIterations 5000
powershell -NoProfile -ExecutionPolicy Bypass -File bench/check-mip-objectives.ps1
# 小规模实例的满预算口径（完成标准"M5 ①：小规模实例求到公开已知最优值"的证据）
powershell -NoProfile -ExecutionPolicy Bypass -File bench/report-mip.ps1 -Manifest bench/data/instances/small.txt -MaxRows 300 -MaxNodes 30000 -MaxIterations 20000 -Output bench/mip-report-small.md
powershell -NoProfile -ExecutionPolicy Bypass -File bench/check-mip-objectives.ps1 -Report bench/mip-report-small.md
```

**`check-mip-objectives.ps1` 按表头列名读报告，不按列位置。** 它此前是按位置取 "nodes / verified" 的：
报告插入 `cuts` 列之后，它把 `verified` 读成 `nodes`、把 `cuts` 读成 `verified`，对一个 33/33 全部过校验的
最优运行报 `UNVERIFIED` 并非零退出 —— 读数的人坏了，而报告里的数字一个都没变。加列就要重新走一遍这条命令。

**`mip-report.md` 是这条命令生成的**：`noswot` 的三个被拒证书在 M5 第三轮全部定论为内核缺陷
（对偶解在病态基上丢精度、方向求解没有自检、非负性自检的尺度与校验器不一致，见 `docs/history.md`），
32 个实例不再有被拒证书。报告是证据，脚本仍然按纪律拒绝写报告 —— 只要内核退出码非零、条目数不符、
出现被拒证书、或解没通过独立复核，就不写。

`report-solve.ps1` 用 **native release** 目标运行内核：同一实例在默认 wasm 目标上要慢约 6 倍
（实测 `mod010`：wasm 119.6s / native debug 203.6s / native release 18.8s），
报告里的每个结果与耗时都来自 native release。

`report-solve.ps1` 在两种情况下**拒绝写报告**并以非零码退出：内核退出码非零
（崩溃或某个文件解析失败），或解析到的实例数与清单条数不一致。一次 native 崩溃曾被写成
"22/33 实例成功"的报告，而报告是仓库里唯一的实测证据，不能把一次没跑完的运行写成一次跑完的运行。

## 行数上限有两个，别混淆

| 上限 | 位置 | 含义 |
| --- | --- | --- |
| `--max-rows N` | `cmd/parse` | **模型约束数**，超过即报 `solve=skipped`，不进入内核 |
| `--max-iterations N` | `cmd/parse` | 迭代上限，让大实例可以被采样而不是被等待（默认沿用内核自身的 20000） |
| `max_kernel_rows` | `SimplexOptions`（默认 200000） | **内核行数**，超过即返回 `SimplexStatus::TooLarge` |
| `max_factor_entries` | `SimplexOptions`（默认 2×10⁷） | 因子分解的**填充量预算**，超过即分解失败 |

内核行数曾经 = 模型约束数 + **每个有限上界一行**，那套口径下 `fast0507` 是 507 条约束、
63009 个 0/1 变量、内核 **63516 行**。**现在有限上界是界而不是行**（有界变量枢轴，见
`simplex/`），同一实例的内核规模是 **489 行**；唯一还会放大行数的是"下界无界的自由变量 +
有限上界"（两个列之差没有单列界，只能留成显式行）。

**内核行数值得盯，而且不只是内存问题**：实测枢轴数随内核行数走、与列数几乎无关
（小规模覆盖型 LP 探针：20 / 40 / 120 / 240 行 → 57 / 137 / 632 / 2220 枢轴；
20 行时 100 列与 900 列分别是 57 与 35）。所以去掉 `fast0507` 的 63001 行上界行之后，
每次枢轴从约 **47.5 ms 降到约 2.4 ms**（实测 150000 次枢轴 400 秒），**Phase I 首次跑完**
（此前 20000 次上限下 Phase I 还停在人工和约 180）。

**内核规模的历史与每次取舍的理由记在 [`../docs/history.md`](../docs/history.md)**：内核最初求 `m×m` 稠密基逆
（63516 行 ≈ 30.8 GB，native 会在分配时以访问冲突退出），后来改成稀疏 LU 分解（内存 `O(nnz + fill)`），
再后来有限上界不再占行。现在两条边界是 `max_kernel_rows`（在分配之前拒绝）与 `max_factor_entries`
（填充量预算，超预算让分解失败而不是无上限分配）。实测：`30n20b8`（presolve 后 11591 行）16 秒求到最优，
`fast0507`（内核 489 行）能跑完 Phase I。

它现在的边界是 **Phase II 的定价**：Dantzig 按检验数选列，那是改进的**速率**而不是改进本身，
覆盖最多未覆盖行的列速率最大、却常被第一行接近紧的约束卡住（实测：Dantzig 选中的列
`r = −121.1`、步长 `0.00632`、增益 `0.76`，而 24 列抽样里就有 `r = −6`、步长 `1.0`、增益 `6.0`）。
改成按**实测增益** `|r| · step` 选列（`gain_candidates`，每迭代只测一个有界候选集）之后，
同一枢轴预算下的内核目标值：20000 次枢轴 165.115 → **156.778**，45000 次 → **133.032**
（Dantzig 要 250000 次枢轴才到 160.353）；代价是每次枢轴约 2.4 → 14 ms。

**非 `optimal` 的运行不打印目标。** 目标列只在真正解出时才有数字，其余状态印 `-`：
一次失败运行返回的目标是 0，而带 presolve 时 `ReducedModel::objective(0.0)` 会把它翻成
**化简自身的目标常数**，看上去像一个结果。`fast0507` 就因此被记成"Phase I 停滞在 13"
（那 13 是化简固定 8 个变量贡献的目标常数，与迭代次数无关）——先用 `--max-iterations 1`
再摘掉 `--presolve` 就能看到同一运行分别印 `obj=13` 与 `obj=0`。Phase I 的真实进度现在印在
迭代上限消息里（实测人工和），不再需要从别处推断。

四个脚本只用 ASCII 字符：Windows PowerShell 5.1 读取**没有 BOM** 的 UTF-8 脚本时会按 ANSI 解码，
非 ASCII 字符会变成乱码。新增脚本请遵守这一约定。

### 行上限挡住的实例：抬高上限之后是什么样

`solve-report.md` 用的是 `--max-rows 2500`（行上限的检查在 presolve **之后**，量的是归约后真正交给
内核的行数），清单里超过这条线的三个实例被记作 `skipped`。**这些实例（含曾经的第四个 `mod011`，
等式 doubleton 代入把它降到 2 402 行而进入报告）都抬到 `--max-rows 20000` 实测过；`mod011` 后来
进入报告口径并解到最优，其余三个没有一个达到最优**（其余口径不变），而且失败原因不止一种 ——
所以这条线排除的是"已知解不完的东西"，不是"没试过的东西"：

| 实例 | 模型行数 | 矩阵系数（`model` 读出的最小/最大非零，比值） | 抬到 20000 行之后 |
| --- | ---: | ---: | --- |
| `core2536-691` | 2 539 | 0.1 … 100（1e+03）—— 绝大多数行的最大系数就是 1，只有 `UB` 行到 100 | Phase I 走不完：500 枢轴后人工和仍有 1 295.0（2.4 s） |
| `dano3_3` | 3 202 | 0.5 … 1 000（2e+03）—— 行最大系数到 1 000 | Phase I 太慢：2 000 枢轴后人工和 32 658.6（33.2 s） |
| `mod011` | 4 497 | 0.14 … 42 336（3.0e+05）；目标系数另跨 7.9 … 2.7e+06（3.4e+05） | **数值失败**：10 742 枢轴后 `solve=numerical-failure`（"basis became numerically singular"，103 s） |
| `2club200v15p5scn` | 17 013 | 全部是 1（1） | 仍在 Phase II：200 枢轴的内核目标 −23.000000363（7.9 s）；1 000 枢轴那一轮 7.5 分钟（450 s CPU）仍未结束 |

两条读数：① 失败方式**不止一种** —— `mod011` 是数值失败（基在 10 742 次枢轴后变成奇异），
`core2536-691` 与 `dano3_3` 是 Phase I 走不完，`2club200v15p5scn` 尺度完全正常（系数全是 1）、贵在 17 013
行本身；② 系数最宽的是 `mod011`（矩阵 3e+05、目标 3.4e+05），而另外三个的尺度都在 1e+03 以内 —— 所以
"尺度大 ⇒ 解不完"不成立（`dano3_3` 只有 2e+03 也解不完），它只解释了四个里的一个。两侧都对 `2200` 没有
异议：它恰好是"最小覆盖已测最大可解模型"（`cap6000`，2 176 行）之后的第一个整百。

**后续（等式 doubleton 代入落地后）**：`mod011` 在同一 20 000 行口径下从上表的数值失败变成
`solve=optimal`（−62121982.55190222，10 177 枢轴，途中一次奇异由 Bland 重启救回）——presolve 把它的
2 077 条等式 doubleton 行代入消去，基的规模减半后奇异性不再致命。其余三个实例没有等式 doubleton 猎物
（`dano3_3` 684 条全是**不等式**，属隐含界地盘），结局不变。命令（一个实例一行，
`N` 取上表的枢轴预算）：

```powershell
_build/native/release/build/cmd/parse/parse.exe bench/data/instances/<name> `
  --solve --presolve --relax --max-rows 20000 --max-iterations N
```

`core2536-691` 此前另有一条记录：20 000 枢轴的整段预算都花在 Phase I 上（没有结论），与表里 500 枢轴的
采样方向一致。

**这张表的"系数"列在第一次发表时是错的，本表已更正**：第一版用一次性 PowerShell 脚本按 MPS 的固定列位
（24/36 起 12 列）取值，而 `mod011` / `core2536-691` 的这些行是自由格式，于是读出来的是半截 token 拼的
数 —— `core2536-691` 被写成 1e+11、`mod011` 写成 1.9e+07，而 `model` 实际读到的分别是 1e+03 与 3.0e+05
（矩阵）。取这些数的正确做法是把实例交给 `format.parse_mps`，再遍历 `constraint(i).terms` 与
`objective()`，不要用列位去切文本。

### 均衡（行 / 列缩放）实测：救不回任何一个实例，但把 `dano3_3` 的 Phase I 加快了一倍

上表里唯一"尺度大"的实例是 `mod011`，而它的失败方式是**数值**的，所以下一个问题是：把实例先均衡（缩放）
到系数可比，它能不能解出来？做法是把每个实例重写成一个等价模型 —— 行除以该行最大系数、列再除以该列
（行缩放后）最大系数，即 `x = s·x'`、系数 `a·s`、界 `l/s`、`u/s`、右端项 `b/r`，**目标值不变** ——
其余口径与报告一致（`--solve --presolve --relax --max-rows 20000`）。

变换本身先在三个能解的实例上验过（目标值必须不变）：`gt2` 13460.233074418948 → 13460.233074450756、
`danoint` 62.637280418466226 → 62.637280418543554、`mod010` 6532.083391108283 → 6532.083353875393，
相对差都在 1e-8 以内。

| 实例 | 原样 | 均衡后 |
| --- | --- | --- |
| `mod011` | 10 742 枢轴后基奇异 | **仍然数值失败**：9 578 枢轴后基奇异；再把目标系数也除以 2.68e6 后 10 585 枢轴、改由内核终检拦下（scaled residual 7.9e-8、负值与界违反 9.3e-7） |
| `core2536-691` | 500 枢轴后人工和 1 295.0 | **逐位不变**，带不带 presolve 都一样 —— 它的行最大系数本来就是 1，列上一半也在 0.1 的量级，均衡无事可做 |
| `2club200v15p5scn` | 200 枢轴 −23.000000363 | **逐位不变**（系数全是 1） |
| `dano3_3` | 2 000 枢轴后人工和 32 658.6 | **17 343.7**（Phase I 约快一倍）；但 20 000 枢轴的整段预算在约 30 分钟 CPU 内没跑完（每枢轴成本随填充增长），所以它同样没被解出来 |
| `gt2` / `danoint` / `mod010`（对照） | 35 / 1 907 / 3 455 枢轴 | 32 / 1 641 / **4 316** 枢轴（最后一个慢 25%） |

⇒ 均衡**不是覆盖面的杠杆**：没有一个实例因为它被解出来，`mod011` 的数值失败也不是"尺度大"这一件事
（缩放到系数全是 1 之后，它在 1e+04 个枢轴处仍写不出证书）。它真正的效果在 Phase I 的速度上，而且**不是
单调的**（同一张表里 `mod010` 反而慢 25%），所以"按尺度阈值决定要不要缩放"这种规则不能靠一张表拍下来。

### 另外两把"再加一点"的旋钮：枢轴上限与割轮次

覆盖面还剩两条"把某个上限往上调"的路，两条都量过了，结论都是**不值得**。

**① LP 的枢轴上限（`fast0507` 是唯一撞上它的实例）。** 三个点：报告口径 `-MaxIterations 20000` 下停在
164.252869961398；**60 000** 枢轴（647 s）到 139.78200546250716；**150 000** 枢轴（1 587 s，26 分钟）到
133.1772397680534 —— 还在降，但**增益在衰减**：20 000→60 000 每千枢轴买到 0.61 个目标单位，
60 000→150 000 每千枢轴只剩 0.073。也就是说它离最优还有一段，而要到那里至少还得几十万枢轴（按 10.6 ms/枢轴
算是一小时以上一次），代价是把求解报告从约 2 分钟变成一小时以上 ⇒ **枢轴上限维持 20 000**，这条"抬上限拿
覆盖"的路在 LP 侧关闭。

**② MIP 的割轮次。** 11 个到节点预算的实例，在固定 200 节点的口径下比 `--cut-rounds 2` 与 `--cut-rounds 16`：

| 实例 | `--cut-rounds 2` | `--cut-rounds 16` |
| --- | --- | --- |
| `gt2` | 20 条割，界 20869.350537660535 | 30 条割，界 **21099.97754330556**（第 3 轮还买到 10 条，之后饱和） |
| `flugpl` | 18 条割，界 1176093.3333337635 | **逐位相同** |
| `blend2` | 16 条割，界 7.0992203805417535 | **逐位相同** |
| `markshare1` | 15 条割，界 0 | **逐位相同** |

也就是说割的生成器在**第二到第三轮就饱和**：11 个实例的累计割数在轮次 2/4/8/16 上分别是
`flugpl 18/18/18/18`、`blend2 16`、`dcmulti 20`、`markshare1 15`、`markshare2 17`、`misc07 20`、
`noswot 20`、`pk1 20`、`rout 20`，只有 `gt2`（20→30）与 `50v-10`（20→40）多买到一轮。默认的
`cut_rounds = 4`（前两轮无条件、之后按"上一轮把根界抬 ≥ 1.4%"放行）已经落在饱和点上，**再加轮次是纯成本**。

**口径提醒（这轮踩到的）**：把节点预算写成 `--max-nodes (r+2)` 去"测轮次"是错的 —— 那个预算里
**割轮重解和树节点共用一个计数**，于是 `r` 越大、真正走的树节点越少，界的变化里混着"树更深"的效果
（本机实测：同一实例 `r=2` 与 `r=16` 在 `(r+2)` 口径下界能差 0.06%，在固定 200 节点口径下**逐位相同**）。
测轮次必须固定树预算。


## 报告

- **`report.md`：全部报告的入口（先读这一份）。** 它由 `bench/report.ps1` 生成，内容是三件事：
  ① 四份报告各覆盖什么、关键数字是多少、**生成时的提交**；② 按顺序的**一键复现命令**；
  ③ 每份报告**是否仍被当前代码支持**——脚本读取报告自己记下的提交，把该报告依赖的源路径在
  "那个提交..HEAD"之间做差分，**再把工作树里这些路径上的未提交改动（含未跟踪的新文件）并进来**，
  有任何一条就标 **STALE** 并列出改动文件。
  两条都要问：报告是「某个提交跑出来的」，所以**它从来没说过未提交的改动**；而一份数字由**改过的**
  文件跑出来的报告，正是这个索引存在的意义。未跟踪文件也算——`mip/` 里多一个 `.mbt` 就是那次测量
  所在的包的一部分，跟 git 认不认识它无关。
  这条判据故意偏保守：**改了注释也算改**（注释不可能动数字），因为方向要偏安全——只在"可能不再描述当前代码"
  时标出来，绝不在"可能仍然有效"时放行；被标出来的报告只有两种结局：重跑它的命令，或者**测出它要打印的数字没变**
  并把那次测量写在某处。脚本自己在任何 scope 路径之外，所以"脚本被改过"不会把四份报告误标成陈旧。
  脚本在三种情况下**拒绝写**（都用非零码退出且不落盘）：列出的报告文件缺失、某份报告的提交或某个汇总数字
  读不出来（索引宁可不出也不能印一个猜的数）、git 无法回答陈旧性。已验证：缺报告 `exit 2`、无 git 历史 `exit 4`。
- `parse-report.md`：解析报告。含工具链版本与产生它的提交哈希，因此每个数字都可追溯到具体代码；
  记录每个实例的规模、整数列数与校验结论，失败的实例逐条给出原因与位置。
- `mip-report.md`：分支定界报告（行数上限 300、节点预算 **600**、迭代上限 5000）。清单里 32 个实例，逐实例给状态、
  目标值、仍在开的界、`nodes` / `verified` / `cuts` 与点复核结论；脚本在退出码非零、条目数不符、
  出现被拒证书或点没通过复核时**拒绝写报告**。
- `mip-report-small.md`：**小规模实例的满预算口径**（清单 `bench/data/instances/small.txt`、行数上限 300、
  节点预算 **30000**、迭代上限 20000、开根割）。它是完成标准"M5 ①：小规模实例求到公开已知最优值"的证据：
  **10 个实例里 6 个证到公开已知最优值**（`22433` 39 节点 / `khb05250` 113 / `p0201` 577 /
  `gt2` 432 / `flugpl` 13812 / **`blend2` 24940**），`check-mip-objectives.ps1 -Report bench/mip-report-small.md`
  对 6 项**取等**通过。**节点预算 30000 是实测定的**：清单里最深的证明（`blend2`）落在 24 940 个松弛上，
  30000 是最小的整千覆盖值；早先 20000 的口径下同一实例报 `node-limit`（两场实验，旧数字留在 `docs/history.md`）。
  清单文件本身也是证据：它把每个实例**实测的每节点成本**与排除理由写在里面（`rout` 391 ms/节点、
  `misc07` 1000 节点超过 8 分钟 → 满预算的臂跑不起）。
  `blend2` 曾因一条证书拒签被记为 BLOCKED（M5 第十二~十四轮），内核补上**对偶侧自检**后已回到清单；
  它跑空的路上还有过**一格**写不出证书的松弛，第四十三轮给恢复梯子加了第三次走法（同一个模型、Dantzig 定价）
  之后它报 `optimal`（见 `docs/history.md`）。
  `verify` 列因此有两种可能小于 `nodes` 的原因：松弛撞上迭代上限，或该松弛的最优基**写不出**满足
  符号约定的证书（内核把它报成 `NumericalFailure`，搜索按"无结论"计）—— 两种都是"预算没做完的活"，
  而不是"被校验器拒绝的主张"。
- `solve-report.md`：求解报告。记录模式（是否 LP 松弛）、行数上限、**枢轴上限**、工具链版本与提交哈希，
  逐实例给出状态、目标值与枢轴迭代数；非最优结果逐条如实列出，包含内核自检测得的残差与负值。
  **枢轴上限是基准口径的一部分**（报告头部写明，且 `pivots` 列等于上限的实例就是在那里停下的）：
  上限不同的两份报告是两场实验，不能互相比较—— 这条以前只靠人记，现在写在报告里。
  当前这份由 `-Relax -MaxRows 2500 -MaxIterations 20000 -Presolve` 生成（**两个上限现在都是实测定的**：
  枢轴上限 20 000 与同一份清单里 MIP 报告给每个松弛的上限一致——早先那个 1200 的口径下同一清单只有
  18 个最优、3 个到上限；行上限的定标逻辑是"最小覆盖已测最大可解模型"——稀疏 LU 之后内核不再分配稠密基逆，
  实测 `cap6000`（**2176 行**）能在枢轴上限内解完，于是上限先定到 2 200，而它挡住的四个实例抬到 20 000 行之后
  **没有一个达到最优**（逐条实测见上文「行上限挡住的四个实例」，失败原因是数值尺度与 Phase I/枢轴成本，
  不是行数本身）；等式 doubleton 代入落地后，行上限的检查挪到了 presolve **之后**（量的是内核真正拿到的问题），
  `mod011` 归约后 **2 402 行**、在 20 000 枢轴内解到最优，按同一条定标逻辑口径升到 **2 500**——
  归约后 2 505 行的 `core2536-691` 恰在它之外。三场实验的数字都留在 `docs/history.md` 里）：
  **32 个实例全覆盖、28 最优、3 因行数上限跳过、`refused` 归零、1 到枢轴上限（`fast0507`，20 000 枢轴）**，
  29 个实例经过化简、28 个重建解全部通过三项检查、1 次需要 Bland 恢复；总枢轴 **36 010**、
  最大已解模型 **4 497 行**（`mod011` 归约后 2 402 行进入报告并解到最优）。从"上限挡住的"变成"解完的"一共 9 个实例：`danoint`（1907 枢轴）与
  `mod010`（3455 枢轴）是 1200→20000 枢轴那场解放出来的，`qiu` / `blp-ar98` / `dsbmip` / `aflow40b` /
  `bc1` / `mitre` / `cap6000`（1128~2176 行）是 1000→2200 行这场解放出来的。
  **`refused` 归零与两个实例从"上限挡住"变成"解完"是稀疏 LU 与增益定价的直接结果**：`30n20b8`
  （presolve 后 11591 行）16 秒求到最优并进入对拍表；`bienst1` / `bienst2` 需要约 2130 次枢轴，
  在 1200 上限下一直报 `iteration-limit`，改按实测增益选列后各 582 次枢轴解完。
  报告里每行还带 `presolve`（化简前后规模与各项计数）与 `check`（还原解在原模型上的
  最大行/界违反，以及用原模型目标向量重算出的目标值）两列 —— 化简是实验的一部分，
  它的记账必须自己站得住。
  化简对两个大实例的效果正好说明它能做什么、不能做什么：`30n20b8` 被化简掉 **7282 个变量**
  （18380→11098，内核行 18956→11591，降 39%），稀疏 LU 之后这 11591 行 16 秒就解到最优；
  `fast0507` 是集覆盖问题，几乎没有可约的上界行（63009→63001 个变量），化解它需要的不是化简，
  而是把上界当界用（内核行 63490 → 489，已落地）与按实测增益定价（也已在默认里），
  剩下的仍是 Phase II 的枢轴数，见 `docs/roadmap.md`。

## 交叉校验（外部权威，独立于本实现）

`check-relaxation-bounds.ps1` 把求解报告里的目标值与 **MIPLIB 2017 官方最优值表**
（`miplib2017-v26.solu`）对拍。松弛问题的目标值不可能超过原问题最优值，所以每个求到最优的实例
都给出一个可判定不等式；一旦松弛值超过官方最优值，就说明内核错了。

下表是较早口径（1200 枢轴上限、16 个实例）下的一次快照，方向性结论不变。**当前口径的完整对拍就是这条
命令的输出**（`checked 28, violations 0, not in the table 0`，口径见 `solve-report.md` 头部）：

| 实例 | 松弛目标值 | 官方最优值 | 结论 |
| --- | ---: | ---: | --- |
| flugpl | 1 167 185.7256 | 1 201 500 | 松弛 ≤ 最优 |
| blend2 | 6.9157 | 7.598985 | 松弛 ≤ 最优 |
| dcmulti | 183 975.5397 | 188 182 | 松弛 ≤ 最优 |
| fiber | 156 082.5176 | 405 935.18 | 松弛 ≤ 最优 |
| gt2 | 13 460.2331 | 21 166 | 松弛 ≤ 最优 |
| khb05250 | 95 919 464.0001 | 106 940 226 | 松弛 ≤ 最优 |
| markshare1 | 0 | 0.999999999999 | 松弛 ≤ 最优 |
| markshare2 | 0 | 1 | 松弛 ≤ 最优 |
| misc07 | 1 415 | 2 810 | 松弛 ≤ 最优 |
| noswot | −43.000000000053 | −41.00000885 | 松弛 ≤ 最优 |
| p0201 | 6 875.0000006 | 7 615 | 松弛 ≤ 最优 |
| pk1 | 1.37e−12 | 11 | 松弛 ≤ 最优 |
| rout | 981.8643 | 1 077.56 | 松弛 ≤ 最优 |
| 22433 | 21 240.5262 | 21 477 | 松弛 ≤ 最优 |
| **30n20b8** | **1.5664** | **302** | 松弛 ≤ 最优（稀疏 LU 后才进入可解范围） |
| 50v-10 | 2 879.0657 | 3 311.1799841 | 松弛 ≤ 最优 |

这不是正确性证明，但能廉价地排掉一整类错误，而且结论是记录下来的，不是假定的——表里每一行都由
`check-relaxation-bounds.ps1` 从报告文件直接生成并与官方最优值表对拍。`noswot` 一行
也说明了为什么这类对拍值得做：它的松弛值 `-43.000000000053` 比 MIP 最优值 `-41.00000885` 更小
才是正确的方向。`30n20b8` 一行则是稀疏 LU 的成果：它在稠密基逆时代需要约 1 GB 基逆而被拒绝，
现在 16 秒解出、目标值 1.5664 ≤ 302 方向正确。

## 报告原则

1. 只报告**实测**结果，不写没有测量支撑的推测；
2. 失败要显式：超时、迭代上限、数值失败、超出能力边界分别统计，不合并成“其他”；
3. 每次报告携带运行环境（`moon version --all`）与提交哈希，保证可复现；
4. 数据不入库，脚本入库，报告入库。
