形式化设计与形式化验证
优化软件的需求本来就大量使用集合、量词、等式和不等式,因此天然适合在编码前进行形式化设计,并在编码后验证“领域规则、数学表达和程序实现”是否一致。目标不是为整个系统一次性写出机器证明,而是让每条重要规则都有明确前提、可检查语义和可追溯证据。
1. 三个需要对齐的对象
对一条业务规则,区分:
| 层次 | 记号 | 示例 |
|---|---|---|
| 领域命题 | 输入点位于分片域内时,输出等于该处的分段线性曲面值 | |
| 数学规格 | ||
| 可执行实现 | BivariateLinearPiecewiseFunction 的三角形选择与重心插值模型 |
已有领域知识和前提组成集合
实现正确性要求:
也就是:
没有写入
2. 形式化设计流程
2.1 建立规则身份
为规则指定稳定 ID、领域名称、所有者 Context、来源和版本。规则名称应表达业务含义,例如 bandwidth.no_outflow_without_service,而不是实现序号。
2.2 声明候选宇宙与前提
明确:
- 集合、索引及空集合行为;
- 决策变量的定义域;
- 参数单位、范围和空值策略;
- 有限上下界及其推导来源;
- 严格不等式如何转换;
- 求解容差与业务容差。
2.3 写领域谓词
先写不依赖 Big-M、辅助变量或某个求解器的命题。若一条自然语言包含多个“并且”“除非”或例外,应拆成可独立命名的谓词。
2.4 推导数学实现
可以采用两条路线:
- 从
和 演绎出可执行的 ; - 先提出候选
,再证明 。
第二条路线不能停在“常见写法”或“样例能跑”;必须补齐等价条件,尤其是有限上下界和变量定义域。
2.5 映射到 OSPF 构件
- 变量由 Aggregation 或领域 Model 拥有;
- 可复用定义成为具名中间值;
- 约束由具名 Pipeline 注册;
- Context 决定在不同业务模式或求解路径中的注册组合;
- Application 只编排,不重新定义公式。
2.6 建立追溯
把命题 ID、公式、Pipeline、测试、基准数据和变更记录关联起来。任一公式变更都应能找到受影响测试和消费者上下文。
3. 等价性的验证义务
逻辑等价可以拆成两个方向:
也可按规格真值拆成正、负分支:
| 规格 | 实现 | 结论 |
|---|---|---|
| true | true | 正分支通过 |
| true | false | 假阴性,违反完备性 |
| false | false | 负分支通过 |
| false | true | 假阳性,违反可靠性 |
测试不能只覆盖“应当可行”的正例。每条约束至少需要一个违反该规则且尽量满足其他前提的负例。
4. 经典推导一:二元线性分段函数
本节以 ospf-kotlin 当前的“三角剖分 + 重心插值”实现为准。它要证明的不是“若干测试点算得差不多”,而是从业务语义开始,经逻辑析取推导出线性约束,并证明符号模型的全部可行解恰好描述给定三角剖分上的分段线性曲面。
4.1 自然语言规则与前提
令
对输入
必须落在至少一个三角形的二维投影内; - 选定一个包含
的三角形后, 必须等于该三角形三个顶点高度的重心插值; - 若
不属于任何三角形,函数在该点无定义,符号模型必须不可行。
每个二维投影必须非退化:
相邻三角形若共享边或顶点,必须在共享位置上给出相同的
4.2 逻辑语言
先不引入选择变量。对每个三角形
前三个条件说明
这一定义同时表达了定义域:若没有任何
4.3 推导线性不等式
逻辑表达中的“至少一个分支成立”需要转换为混合整数线性模型。为每个三角形引入选择变量:
第一步:只激活一个分片。
若求解器的标准形式只保留不等式,该等式等价于:
第二步:激活对应的重心权重。 为每个顶点引入
当
第三步:合并各分支的坐标与高度。
定义:
关联输入输出:
每个等式
至此,逻辑析取已经被完整转换为二值变量、非负变量以及线性不等式。该推导不需要任意取大的 Big-M。ospf-kotlin 中,zVars 承担 lambdaVars 承担
4.4 等价性证明
可靠性
结合
完备性
因此在非退化与重叠一致性前提
可靠性保证模型不接受定义域外或插值错误的点;完备性保证逻辑规格允许的每个曲面点都能由线性模型表示。
4.5 从证明得到的测试
使用三角形:
该分片上的曲面为
验证至少覆盖:
| 类别 | 输入 | 应验证 |
|---|---|---|
| 三个顶点 | 权重 one-hot,结果等于顶点高度 | |
| 内部点 | 三个权重非负且和为 1, | |
| 共享边 | 一个权重为 0 | 相邻分片给出相同 |
| 域外点 | 不属于任何三角形 | 直接求值返回空,符号模型不可行 |
| 退化三角形 | 构造/求值按契约拒绝,不参与证明 | |
| 重叠分片 | 同一 | 要么拒绝,要么证明重叠处高度一致 |
| 双路径一致性 | 同一输入 | evaluate 结果与 solver 固定输入后的 |
更多 API 与边界见二元线性分段函数。
5. 经典推导二:建议载重量
这个例子关注一条业务规则如何逐层收敛为可执行约束,而不讨论代码复用或中间值的实现方式。
5.1 自然语言规则
对每个装载位置
- 若该位置未被选为建议装载位置,则其建议载重量必须为 0;
- 若该位置被选中,则其建议载重量必须处于允许区间
; - 允许区间必须使用统一的重量单位,并满足
。
自然语言中的“若……则……”和“处于区间”必须先转换为逻辑命题,不能直接跳到 Big-M 约束。
5.2 逻辑语言
定义:
表示位置
也可以写成两条蕴含:
因为
5.3 推导线性不等式
先处理上界。两种逻辑状态所要求的上界分别为:
由于
再处理下界。两种状态所要求的下界分别为:
它们可以合并为:
因此最终的线性实现为:
这里的
5.4 等价性证明
可靠性
- 若
,线性约束化为 ,所以 ; - 若
,线性约束化为 。
两个二值分支都满足领域谓词,因此线性实现不会接受违反规则的解。
完备性
- 若领域状态为
,代入后得到 ; - 若领域状态为
,代入后恰好得到原区间。
因此在前提
下,有:
5.5 从证明得到的测试
设
| 期望 | 验证分支 | ||
|---|---|---|---|
| 0 | 可行 | 未选中的正例 | |
| 0 | 不可行 | 未选中的负例 | |
| 1 | 可行 | 闭区间下边界 | |
| 1 | 可行 | 区间内部 | |
| 1 | 可行 | 闭区间上边界 | |
| 1 | 不可行 | 下界外侧 | |
| 1 | 不可行 | 上界外侧 |
被错误放宽为连续变量时,等价性测试能够失败; - 下界、上界或重量值单位不一致时,在进入模型前被拒绝或换算;
的数据在初始化阶段被拒绝; - 多个位置组成的小规模实例中,逐点比较
与 的真假值; - 固定
后,Kotlin 与 Rust 的 solver 测试得到相同可行性结论。
6. 五层验证
| 层次 | 验证对象 | 推荐证据 |
|---|---|---|
| 1. 领域谓词 | 自然语言与形式化规格一致 | 领域评审、真值表、反例 |
| 2. 数学推导 | 代数证明、SMT/穷举、小规模反证模型 | |
| 3. 符号构造 | OSPF 表达式等于数学实现 | 系数、常数项、上下界和约束方向快照 |
| 4. 求解器契约 | 编译后的模型实现符号语义 | 微型可行/不可行模型、多后端测试 |
| 5. 应用行为 | 上下文组合和解分析正确 | 基准实例、回归集、领域不变量 |
层次 3 通过,不代表层次 2 正确;求解器得出预期目标,也不能代替负分支验证。
7. 一致性、冗余与冲突
向模型增加
一致性
若不可满足,应给出不可满足核心或最小冲突规则集,并用领域名称报告。
冗余
冗余约束不改变可行域。它可能为了强化 LP、提高可读性或诊断而保留,但应记录目的,避免误认为它增加了业务规则。
冲突
这说明新增规则否定了所有既有可行情况。若这是业务变更,应同步更新被替代的规则和基准,而不是简单叠加。
8. 数值语义
形式化规格使用精确关系,浮点求解器使用容差。验证报告必须同时给出:
- 规格关系,例如
; - 求解器容差
; - 测试判定容差
; - 业务容差
; - 变量缩放和单位。
不要使用正好落在容差灰区内的数值作为唯一负例。测试等式时,应分别覆盖
9. 分解算法中的验证
列生成
需要证明 Pricing 的领域可行列与 Master 接受的列定义一致,并验证:
在两侧使用同一成本、系数和对偶符号约定。小实例上应与完整列枚举比较。
Benders
需要证明选择性注册与完整模型等价、固定变量映射保持语义,以及每条切割对所有原问题可行解都有效。可行性切割必须排除当前不可行主解;最优性切割必须在生成点提供正确的追索下界。
10. 变更控制
规则变更时按以下顺序处理:
- 更新领域命题、前提和版本;
- 重新检查一致性、冗余和受影响推论;
- 更新数学推导及中间值接口;
- 修改 Pipeline 或函数符号实现;
- 从新的证明分支更新测试;
- 运行上下文、完整模型、分解模型和后端回归;
- 在发布说明中记录可行域或目标语义是否变化。
如果只优化实现而不改变
11. 完成标准
一条规则只有同时满足以下条件才算完成:
- 有稳定 ID、所有者和业务描述;
- 前提、单位、定义域与容差完整;
- 数学规格和可执行表达均已记录;
- 等价性的两个方向有证明或足够的可检查证据;
- 正、负、边界和异常输入测试齐全;
- 能从文档追溯到中间值、Pipeline、测试和基准;
- 在实际使用的求解器后端上通过契约测试。