使用 Z3 SMT 解决谓词演算问题

Posted

技术标签:

【中文标题】使用 Z3 SMT 解决谓词演算问题【英文标题】:Solving predicate calculus problems with Z3 SMT 【发布时间】:2021-12-04 23:45:56 【问题描述】:

我想使用 Z3 解决最自然地以原子(符号)、集合、谓词和一阶逻辑表示的问题。例如(在伪代码中):

A = a1, a2, a3, ... # A is a set
B = b1, b2, b3...
C  = c1, c2, c3...

def p = (a:A, b:B, c:C) -> Bool # p is unspecified predicate
def q = (a:A, b:B, c:C) -> Bool

# Predicates can be defined in terms of other predicates:
def teaches = (a:A, b:B) -> there_exists c:C 
                            such_that [ p(a, b, c) OR q(a, b, c)  ]

constraint1 = forall b:B there_exists a:A
                         such_that teaches(a, b)

solve(constraint1)

在 Z3(或其他 SMT)中表达原子、集合、谓词、关系和一阶量词的好方法是什么?

这有标准的成语吗?必须手动完成吗?是否有可以转换它们的翻译库(不一定特定于 Z3)?

我相信 Alloy 使用 SMT 来实现谓词逻辑和关系,但 Alloy 似乎更多地是为交互式使用而设计的,以探索模型的一致性,而不是为问题找到特定的解决方案。

【问题讨论】:

我添加了一个可以更直接地解决您的问题的示例,使用惯用的 SMTLib。希望对您有所帮助。 【参考方案1】:

“Alloy 似乎更多地是为交互式使用而设计的,以探索模型的一致性,而不是为问题找到特定的解决方案。”

恕我直言,Alloy 在验证您自己的思维方式时大放异彩。您对某些东西进行建模,并且通过几个实例的可视化,您有时会意识到您建模的内容并不完全是您所希望的。 从这个意义上说,我同意你的看法。

然而,Alloy 也可用于找到问题的具体解决方案。您可以使用约束重载模型,以便只能找到一个实例(即您的解决方案)。 当您的域空间相对较小时,它也能很好地工作。

这是用合金翻译的模型:

sig A,B,C

pred teaches(a:A,b:B) 
some c:C | a->b->c in REL.q or a->b->c in REL.p

// I'm a bit rusted, so .. that's my unelegant take on defining an "undefined predicate"
one sig REL 
q: A->B ->C,
p: A->B->C


fact constraint1 
all b:B | some a:A | teaches[a,b]


run 

如果你想自己定义集合 A、B、C 中的原子并在谓词中引用它们,你总是可以像下面这样过度约束这个模型:

abstract sig A,B,C

one sig A1,A2 extends A
one sig B1 extends B
one sig C1,C2,C3 extends C


pred teaches(a:A,b:B) 
some c:C | a->b->c in REL.q or a->b->c in REL.p

one sig REL 
q: A->B ->C,
p: A->B->C

// here you could for example define the content of p and q yourself
q= A1->B1->C2 + A2 ->B1->C3
p= A1->B1->C3 + A1 ->B1->C2
 

fact constraint1 
all b:B | some a:A | teaches[a,b]


run 

【讨论】:

这很棒。有没有办法在 Z3 中做类似的事情? 不幸的是,我从未与 Z 合作过 :-(.【参考方案2】:

在 SMTLib 中建模谓词逻辑确实是可能的;虽然与 Isabelle/HOL 等常规定理证明器相比可能有点麻烦。解释结果可能需要相当多的眯眼。

话虽如此,下面是使用 SMTLib 对您的示例问题进行的直接编码:

(declare-sort A)
(declare-sort B)
(declare-sort C)

(declare-fun q (A B C) Bool)
(declare-fun p (A B C) Bool)

(assert (forall ((b B))
  (exists ((a A))
    (exists ((c C)) (or (p a b c) (q a b c))))))

(check-sat)
(get-model)

几点说明:

declare-sort 创建一个未解释的排序。它本质上是一组非空值。 (也可以是无限的,除了它不是空的事实之外,没有做出任何基数假设。)对于您的具体问题,实际上这种类型似乎并不重要,因为您没有使用它的任何一个直接元素。如果这样做,您可能还想尝试“声明”排序,即数据类型声明。这可以是一个枚举,或者更复杂的东西;取决于问题。对于提出的当前问题,未解释的排序就可以了。

declare-fun 告诉求解器有一个具有该名称和签名的未解释函数。但除此之外,它既不定义它,也不以任何方式限制它。您可以添加关于它们的“公理”,以更具体地了解它们的行为方式。

支持量词,正如您在constraint1 的编码方式中看到的forallexists。请注意,SMTLib 不太适合代码重用,通常在更高级别的绑定中进行编程。 (提供了来自 C/C++/Java/Python/Scala/O'Caml/Haskell 等的绑定,具有相似但不同程度的支持和特性。)否则,它应该易于阅读。

我们最终发出check-satget-model,要求求解器创建一个满足所有断言约束的宇宙。如果是这样,它将打印sat 并有一个模型。否则,如果没有这样的宇宙,它将打印unsat;或者如果它无法决定,它也可以打印unknown(或永远循环!)。 SMT 求解器难以处理量词的使用,大量使用量词无疑会导致unknown 成为答案。这是一阶谓词演算的半可判定性的固有局限性。

当我通过 z3 运行此规范时,我得到:

sat
(
  ;; universe for A:
  ;;   A!val!1 A!val!0
  ;; -----------
  ;; definitions for universe elements:
  (declare-fun A!val!1 () A)
  (declare-fun A!val!0 () A)
  ;; cardinality constraint:
  (forall ((x A)) (or (= x A!val!1) (= x A!val!0)))
  ;; -----------
  ;; universe for B:
  ;;   B!val!0
  ;; -----------
  ;; definitions for universe elements:
  (declare-fun B!val!0 () B)
  ;; cardinality constraint:
  (forall ((x B)) (= x B!val!0))
  ;; -----------
  ;; universe for C:
  ;;   C!val!0 C!val!1
  ;; -----------
  ;; definitions for universe elements:
  (declare-fun C!val!0 () C)
  (declare-fun C!val!1 () C)
  ;; cardinality constraint:
  (forall ((x C)) (or (= x C!val!0) (= x C!val!1)))
  ;; -----------
  (define-fun q ((x!0 A) (x!1 B) (x!2 C)) Bool
    (and (= x!0 A!val!0) (= x!2 C!val!0)))
  (define-fun p ((x!0 A) (x!1 B) (x!2 C)) Bool
    false)
)

这需要稍微眯眼才能完全理解。第一组值告诉您求解器如何为未解释的排序 ABC 构建模型;带有见证元素和基数约束。您可以在很大程度上忽略这部分,尽管它确实包含有用的信息。例如,它告诉我们A 是一个包含两个元素的集合(名为A!val!0A!val!1),C 也是如此,而B 只有一个元素。根据您的限制,您将获得不同的元素集。

对于p,我们看到:

  (define-fun p ((x!0 A) (x!1 B) (x!2 C)) Bool
    false)

这意味着p 总是False;即,无论传递给它的参数是什么,它都是空集。

对于q,我们得到:

 (define-fun q ((x!0 A) (x!1 B) (x!2 C)) Bool
    (and (= x!0 A!val!0) (= x!2 C!val!0)))

让我们更简单地重写一下:

 q (a, b, c) = a == A0 && c == C0

其中A0C0 分别是AC 的成员;请参阅上面的排序声明。所以,只要aA0cC0,它就会说qTrue,而b 是什么并不重要。

你可以说服自己这个模型确实满足你想要的约束。

总结一下;在 z3 中对这些问题进行建模确实是可能的,尽管有点笨拙和大量使用量词会使求解器永远循环或返回 unknown。解释输出可能有点麻烦,但您会意识到模型将遵循类似的模式:首先是未解释的排序,然后是谓词的定义。

旁注

正如我所提到的,在 SMTLib 中对 z3 进行编程既麻烦又容易出错。这是使用 Python 接口完成的相同程序:

from z3 import *

A = DeclareSort('A')
B = DeclareSort('B')
C = DeclareSort('C')

p = Function('p', A, B, C, BoolSort())
q = Function('q', A, B, C, BoolSort())

dummyA = Const('dummyA', A)
dummyB = Const('dummyB', B)
dummyC = Const('dummyC', C)

def teaches(a, b):
    return Exists([dummyC], Or(p(a, b, dummyC), q(a, b, dummyC)))

constraint1 = ForAll([dummyB], Exists([dummyA], teaches(dummyA, dummyB)))

s = Solver()
s.add(constraint1)
print(s.check())
print(s.model())

这也有它的一些特点,但如果您选择用 Python 对 z3 进行编程,希望它能为您的探索提供一个起点。这是输出:

sat
[p = [else -> And(Var(0) == A!val!0, Var(2) == C!val!0)],
 q = [else -> False]]

它与 SMTLib 输出具有完全相同的信息,但编写方式略有不同。

函数定义样式

请注意,我们将 teaches 定义为常规 Python 函数。这是 z3py 编程中的常用风格,因为它产生的表达式在调用时被替换。您也可以创建一个 z3 函数,如下所示:

teaches = Function('teaches', A, B, BoolSort())
s.add(ForAll([dummyA, dummyB],
       teaches(dummyA, dummyB) == Exists([dummyC], Or(p(dummyA, dummyB, dummyC), q(dummyA, dummyB, dummyC)))))

请注意,这种定义风格将依赖于内部的量词实例化,而不是 SMTLib 的通用函数定义工具。因此,您通常应该更喜欢 python 函数样式,因为它可以转换为“更简单”的内部构造。通常,它也更容易定义和使用。

需要 z3 函数定义样式的一种情况是,如果您定义的函数是递归的,并且它的终止依赖于符号参数。有关此问题的讨论,请参阅:https://***.com/a/68457868/936310

【讨论】:

这是一个绝妙的答案。为什么选择将teaches 实现为 Python 方法(Z3 工厂?)而不是直接在 Z3 API 中实现?是不是 Z3 API 只允许你对函数施加约束(f(x) == y)而不是全部定义它们? 这是一个风格问题,但它也会导致更简单的 z3 程序。我在答案末尾添加了对此的讨论。

以上是关于使用 Z3 SMT 解决谓词演算问题的主要内容,如果未能解决你的问题,请参考以下文章

SMT 语法中的矩形拟合(Z3 求解器)

如何在NLP中使用概念依赖,谓词演算/逻辑,概念图

人工智能原理复习 | 命题逻辑和谓词演算

Gecode branch()函数的z3替代方案?

用于 SMT 中的所有用途

谓词逻辑