Skip to content

Latest commit

 

History

History
292 lines (200 loc) · 14.5 KB

lab3_review.md

File metadata and controls

292 lines (200 loc) · 14.5 KB

Lab 3 实验回顾:谓词逻辑与 SMT 求解

实验报告:lab3/report.md

本文记录现有代码的运行方法、各任务的操作和思路,以及整理回顾时的实际复跑结果。原报告和实验代码未作修改。命令用于复现;模型的打印顺序和具体方案可能因求解器版本等因素变化,应检查约束是否满足,而不是要求输出逐字相同。

1. 整体思路

实验主线是:自然语言要求 → 选择合适的变量类型 → 写出约束 → 求解 → 把模型还原为具体方案 → 独立核验和解释结果。

任务 要回答的问题 核心方法
1 同一个部署问题如何用两种模型表达? 12 个 Bool 编码位置,或 4 个 Int 直接表示节点
2 给定的排课方案是否合法? C++ 检查范围和冲突
3 能否从规则中自动找到排课方案? 整数范围与冲突不等式
4 加一后是否一定更大? 否定性质,寻找边界反例
5 规则能否推出模块发布结论? 谓词、量词、性质否定
6 部署要求为什么无解? 不可满足核与逐条删减
7 合法方案中最少需要几个时间段? Optimize 最小化实际使用数量

需要区分两类求解:

  • 寻找方案:检查约束 C,sat 表示存在满足要求的方案。
  • 验证性质:检查 C 与 Not(P),sat 表示存在违反 P 的反例;unsat 表示 C 能够推出 P。

因此,任务 5 输出“存在可发布模块:unsat”并不是说没有模块可发布,而是说“不存在可发布模块”这一反例情形无法成立。前提是基础约束自身可满足。若求解器返回 unknown,不能按 sat 或 unsat 解读。

2. 环境与实际运行方式

使用 Windows、PowerShell、Python 虚拟环境、z3-solver 和支持 C++17 的 G++。

从仓库根目录进入实验目录并激活已有虚拟环境:

Set-Location D:\ME\FormalMethods\lab3
.\.venv\Scripts\Activate.ps1
python z3_smoke.py

环境检查输出应为:

Z3 smoke test passed

如果不想激活虚拟环境,也可以直接指定解释器。这也是本次复跑 Python 脚本采用的方式:

Set-Location D:\ME\FormalMethods\lab3
.\.venv\Scripts\python.exe -B z3_smoke.py
.\.venv\Scripts\python.exe -B mission3_2.py

-B 表示不生成 Python 字节码缓存,不改变脚本的求解逻辑。本次通过一个外部 Python 驱动程序依次启动现有 14 个脚本,每个脚本单独启动解释器、收集输出并检查退出码,未改动脚本。下文为便于手动操作,将各次运行列为单独的 python 文件名.py 命令。

若新建环境,可使用以下命令准备依赖;这些是环境重建说明,不代表本次重复安装了依赖:

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install z3-solver

安装包名是 z3-solver,代码中导入的是 z3。应确认安装依赖和运行脚本使用同一个 Python 环境。

3. 任务 1:比较布尔 CNF 与整数 SMT

python mission1.py
python mission1_2.py

操作和建模

布尔模型中,X_G_0 等变量表示某组件是否位于某节点,共 12 个 Bool。每个组件使用一条“至少一个”子句和三条“两两不能同时选择”子句,表达恰好部署在一个节点。

五条业务要求也转换为子句。例如:

网关与数据库不在节点 0 共存:Not(g0) Or Not(d0)
认证位于节点 0 则数据库也在:Not(a0) Or d0
数据库位于节点 0 则认证也在:Not(d0) Or a0
缓存在节点 1 则网关在节点 0:Not(c1) Or g0

每次 s.add(...) 加入必须满足的约束,所有约束之间是合取。因此无需输出 DIMACS 文件,也可以直接把 CNF 表达式交给 Z3。

整数模型直接用 G、A、D、C 表示所在节点,限制在 0..2 内;相同节点写 A == D,不同节点写 G != D,条件要求写 Implies(C == 1, G == 0)。

复跑结果与检查

两份脚本均返回 sat。布尔模型给出 G=0、A=1、D=1、C=1;整数模型给出 G=1、A=0、D=0、C=2。两份方案不同,但都满足五条规则,说明无需要求两次求解返回相同模型。

当前 mission1.py 对网关位置同时加入 Not(g2) 和 Or(g0, g1)。在已有“恰好一个节点”约束下,两者等价,因此有一条冗余约束:当前文件实际包含 29 条子句;只保留其中一种写法时是此前讲解的 28 条。冗余不影响模型正确性,本次未修改代码。

思路: 布尔编码需要显式描述各种位置选择,整数 SMT 可以直接表达节点值及其关系。表达更简洁不等于已经证明求解更快。

4. 任务 2:用 C++ 独立检查给定方案

手动编译和运行方式为:

g++ -std=c++17 lab3_solution.cpp test2.cpp -o test2.exe
.\test2.exe

两个文件需要一起编译:lab3_solution.cpp 提供 validSchedule 实现,test2.cpp 提供函数声明、测试数据和 main。提交用的函数文件不含 main,也不执行标准输入输出。

本次复跑使用相同的两个源文件,把可执行文件输出到系统临时目录,避免覆盖实验目录中已有的 test2.exe:

$testBinary = Join-Path $env:TEMP 'formalmethods-lab3-review-test2.exe'
g++ -std=c++17 lab3_solution.cpp test2.cpp -o $testBinary
& $testBinary

函数先检查所有值是否满足 0 <= slot[i] < slotCount,再检查每条冲突是否满足 slot[u] != slot[v]。任意检查失败就返回 false,全部通过才返回 true。

测试 slot 实际返回 结果
合法方案 [0,1,2,0,0,1] true PASS
编号越界 [3,1,2,0,0,1] false PASS
冲突课程同时上课 [0,0,2,0,0,1] false PASS
非冲突课程共享时间段 [0,1,1,0,0,1] true PASS

最后一例中课程 1、2 都在时间段 1,但原始冲突列表没有 (1,2),所以合法。不能要求所有课程时间段互不相同,也不能将课程数量写死为六。

5. 任务 3:让 Z3 自动寻找排课方案

python mission3.py
python mission3_2.py

操作和结果

为六门课程分别声明 Int,将时间段限制在 0..2,对七条冲突分别加入不等式。mission3.py 复跑返回 sat,得到:

slot = [1, 0, 0, 1, 1, 2]
时间段 0:课程 [1, 2]
时间段 1:课程 [0, 3, 4]
时间段 2:课程 [5]

model.eval(s0).as_long() 把模型中的 Z3 数值读取为普通 Python 整数。六个值必须按课程编号顺序提取,不能按模型打印顺序直接抄写。

mission3_2.py 在原始七条冲突上新增 (0,3),每次创建新 Solver,分别限制在两个和三个时间段内:

2 个时间段:unsat
3 个时间段:sat
slot = [0, 2, 2, 1, 1, 2]

集合 {0,1,3} 中存在 (0,1)、(1,3)、(0,3),三门课程两两冲突,至少需要三个时间段。任意两门课都能用两个时间段安排,所以三门课已经是一个最小的不可二时间段排课集合。实际三时间段方案的存在说明整个问题在三个时间段内可行。

独立检查的边界

任务要求将 Z3 输出方案交给 validSchedule。当前 mission3.py 只输出方案和分组,mission3_2.py 只输出求解结果和方案;它们没有自动调用 C++。当前 test2.cpp 也只检查固定四个用例。本次复跑不将固定用例通过写成“已调用 C++ 检查全部 Z3 输出方案”。

复现这一环节时,应另行在测试程序中用实际输出构造 std::vector<int> 并调用检查函数;检查新增冲突后的方案,还要把 (0,3) 加入传入的冲突列表。也可人工逐条检查范围和全部冲突,但人工检查不替代手册要求的函数调用。

思路: 任务 2 是“方案 → 检查规则”,任务 3 是“规则 → 求出方案”。范围 0..2 只意味着允许三个时间段,不保证求解器实际用满三个。

6. 任务 4:比较 Int 与 8 位 BitVec

python unsigned_increment.py
python signed_increment.py
python mission4.py

三个脚本都检查“加一后更大”的否定,而不是直接断言性质成立。

语义 比较表达式 实际求解结果 反例及解释
8 位无符号 Not(UGT(y, x)) sat,x=255 加一只保留低 8 位,得到 0
8 位有符号 Not(y > x) sat,x=127 加一得到位模式 10000000,按有符号解释为 -128
数学整数 Not(y > x) unsat Int 不发生固定位宽回绕,找不到反例

当前两个 BitVec 脚本只打印模型中的 x,不会单独打印 y,因为 y 是 x + 1 表达式。表中的 y 来自位运算边界的独立计算。位向量的 > 是有符号比较,UGT 是无符号比较;二者并未改变加法保留低 8 位的机制。

思路: 验证前先确定数值语义。不能把数学整数的结论直接套到机器位宽模型上,也不要把所有定宽运算结论直接当作 C++ 源程序语义的结论。

7. 任务 5:谓词、量词与发布条件

python mission5.py
python mission5_2.py

EnumSort 定义仅含 Parser、Database、UI 的模块类型。Function(..., Module, BoolSort()) 声明模块到真假的谓词,ForAll 表示所有模块遵守规则,Exists 表示至少存在一个模块。

两条规则分别为“通过测试则可以发布”和“可以发布则已经通过测试”,共同要求 Tested 与 Releasable 一致。

mission5.py 的事实是 Parser、UI 通过测试,Database 未通过测试。三个独立反例检查的实际结果为:

待检查性质 P 检查基础约束与 Not(P) 结论
存在可发布模块 unsat 性质成立
Database 不可发布 unsat 性质成立
所有模块都可发布 sat Database 构成反例

输出模型中 Parser、UI 的 Tested 和 Releasable 均为 True,Database 的二者均为 False。这与前两项的 unsat 完全一致,因为 unsat 描述的是性质否定无解,不是性质为假。

mission5_2.py 将 Database 的事实替换为已通过测试,再检查全部三个性质,实际结果依次为 unsat、sat、unsat。所有模块都可发布;“Database 不可发布”反而有反例。不能保留旧的否定事实再追加肯定事实,否则基础模型本身就矛盾了。

8. 任务 6:不可满足核与逐条删除检查

python deployment_core.py
python mission6.py

deployment_core.py 用 assert_and_track 给每条要求附上标签,求解为 unsat 后用 unsat_core() 取得冲突标签。本次复跑得到六条容量相关标签:

api_high_availability
worker_matches_api
database_high_availability
standby_required
one_service_per_node
maintenance_capacity

它们共同推出 API 至少 2、Worker 至少 2、数据库至少 2、备用节点至少 1,总需求至少为 7,与最多 6 个节点矛盾。监控和审计两条要求未出现在核中。

mission6.py 每次创建新求解器,保留全部领域约束,从完整要求中单独删除一条。它遍历全部八条要求,因此包括题目要求的六条核约束删减,也额外检查了两条非核约束:

单独删除项 实际结果
api_high_availability sat
worker_matches_api sat
database_high_availability sat
standby_required sat
one_service_per_node sat
maintenance_capacity sat
monitoring_required unsat
monitoring_requires_audit unsat

例如不要求备用节点时可以取 2+2+2+0=6。删除监控要求不能降低副本和备用节点需求,所以容量矛盾仍然存在。删除总节点计算关系后得到 sat,只说明删减后的模型可满足,不能据此声称原部署规则仍被完整满足。

思路: 六条核约束分别删除后都变为 sat,说明当前核在固定领域约束的前提下不可继续删减。这是包含意义上的最小性,不是一般意义上已证明所有核中条数最少。某条约束未出现在当前核中,也不能推广为它在所有需求分析中都没有作用。

9. 任务 7:从可满足到最优

python mission7.py
python mission7_2.py

先建立与原始排课问题相同的范围和七条冲突,再对每个时间段定义:

used[t] == Or(slot[0] == t, ..., slot[5] == t)
objective = Sum(If(used[t], 1, 0))

used[t] 必须与“至少一门课使用该时间段”真假一致,不能仅靠单方向蕴含代替。If 将真假转为 1 或 0,Sum 计算实际使用数量。随后用 Optimize() 和 minimize(objective) 寻找最小值。

本次复跑:

模型 最少时间段数 一份实际模型,按课程 0..5 排列
原始七条冲突 2 [1,2,2,1,1,2]
原始冲突新增 (1,2) 3 [1,2,0,1,2,0]

原始问题至少有一条冲突边,因此一个时间段不够;两时间段方案存在,说明最优值为 2。新增 (1,2) 后,集合 {0,1,2} 两两冲突,给出至少三个时间段的下界;三时间段方案存在,说明下界可达,最优值为 3。

任务 7 新增的是 (1,2),任务 3 新增的是 (0,3),两者都应分别从原始七条冲突出发。

当前两个优化脚本打印模型和目标值,没有内置逐条冲突检查输出。为独立核验,本次对表中的两组方案人工逐条代入:

冲突对 原方案两端时间段 新方案两端时间段
(0,1) 1、2 1、2
(0,2) 1、2 1、0
(1,3) 2、1 2、1
(2,3) 2、1 0、1
(2,4) 2、1 0、2
(3,5) 1、2 1、0
(4,5) 1、2 2、0
新增 (1,2) 不适用于原问题 2、0

全部适用冲突的两端均不相等,且两组方案的值均在 0..2 内。原方案只使用 {1,2},新方案使用 {0,1,2},计数与目标值一致。不能用最大时间段编号加一直接代表实际使用数量,例如原方案最大编号为 2,却只使用两个时间段。

10. 复跑范围与报告记录

本次实际执行 14 个现有 Python 脚本,均正常退出;同时以 C++17 编译现有两个 C++ 文件,四个固定测试全部 PASS。Python 输出、C++ 返回值和上述人工核验分别提供不同层面的证据,不能将其中一种替代另一种而不说明。

使用 LLM 辅助时,应记录真实提示词、实际采用或修改的代码,以及独立核验方式。有效的核验包括边界计算、逐条代入业务规则、使用独立检查函数,以及“下界证明 + 达到下界的方案”。截图用于保存实际执行证据,不能只靠预期输出充当运行结果。