原文:https://ericpony.github.io/z3py-tutorial/guide-examples.htm
练习:http://3xp10it.cc/auxilary/2017/11/14/z3-solver%E5%AD%A6%E4%B9%A0/
注:该翻译用于自嗨
Python中使用Z3 API
z3是Microsoft Research开发的高性能的定理证明工具。z3常常被用在许多应用中,如:软件/硬件的验证和测试,约束求解问题,混合系统分析,安全领域,生物学和几何问题。
本教程演示z3py的主要功能,即在python中使用z3 api,请务必跟着一起动手实验。
开始
我们来看一个简单的例子:
x = Int('x')
y = Int('y')
solve(x > 2, y < 10, x + 2*y == 7)
Int('x')函数创建了一个z3的整形变量名为x,solve函数求解了一个约束系统,这个例子使用了两个变量x和y,以及3个约束条件。z3py和python一样使用"="来进行赋值,使用<, <=, >, >=, ==, !=来进行比较。在以上例子中,表达式"x + 2*y == 7"是一个z3约束条件。
默认的,z3py显示公式或表达式时使用数学的标记法,通常用∧来表示逻辑符号与,用∨来表示逻辑符号或,等等。命令set_option(html_mode=True/False)可以用来设置表达式的格式,True使用数学标记法,而False使用z3py的标记法,这也是离线版的z3py默认的模式
x = Int('x')
y = Int('y')
print x**2 + y**2 >= 1
set_option(html_mode=False)
print x**2 + y**2 >= 1
z3提供了一些函数来遍历表达式内容
x = Int('x')
y = Int('y')
n = x + y >= 3
print "num args: ", n.num_args()
print "children: ", n.children()
print "1st child:", n.arg(0)
print "2nd child:", n.arg(1)
print "operator: ", n.decl()
print "op name: ", n.decl().name() #返回str
z3提供所有基本的数学运算符。z3py使用与python相同的运算优先级。和python一样,**代表幂运算。z3可以求解非线性的多项式条件约束。
x = Real('x')
y = Real('y')
solve(x**2 + y**2 > 3, x**3 + y < 5)
过程Real('x')创建了一个实数变量x。z3py可以表示任意大的整数,有理数和无理数。一个无理数是整数系数多项式的根。在内部,z3py精确的表示这些数,无理数以十进制小数的方式来表示,这方便读取结果。
x = Real('x')
y = Real('y')
solve(x**2 + y**2 == 3, x**3 == 2)
set_option(precision=30)
print "Solving, and displaying result with 30 decimal places"
solve(x**2 + y**2 == 3, x**3 == 2)
过程set_option用于设置z3的执行环境。它用于设置全局配置选项,如以何种方式来显示结果,选项precision=30,设置十进制显示结果中的小数位数。1.2599210498?中的?标志输出被截断。
以下例子演示了一个常见的错误,表达式1/3是一个python整形数字而非一个z3有理数,这个例子也体现出了使用z3py创建有理数的不同方式。过程Q(num, den)创建了一个z3有理数,该有理数以num作为分子,den作为分母。过程RealVal(1)创建了一个z3实数来表示数字1。
print 1/3
print RealVal(1)/3
print Q(1,3)
x = Real('x')
print x + 1/3
print x + Q(1,3)
print x + "1/3"
print x + 0.25
有理数也可以表示为十进制小数形式
x = Real('x')
solve(3*x == 1)
set_option(rational_to_decimal=True)
solve(3*x == 1)
set_option(precision=30)
solve(3*x == 1)
一个约束系统可能无解,这种情况下,我们称该系统为不可满足的。
x = Real('x')
solve(x > 4, x < 0)
和python相同,注释以#开头,以换行符结束,z3py不支持多行注释。
BooLean Logic
z3支持布尔运算符:And,Or,Not,Implies,If。等价使用==来表示。
以下例子显示了如何求解简单的布尔条件约束
p = Bool('p')
q = Bool('q')
r = Bool('r')
solve(Implies(p, q), r == Not(q), Or(Not(p), r))
python布尔常量True和False可以创建z3的布尔表达式
p = Bool('p')
q = Bool('q')
print And(p, q, True)
print simplify(And(p, q, True))
print simplify(And(p, False))
以下例子结合了多项式和布尔约束
p = Bool('p')
x = Real('x')
solve(Or(x < 5, x > 10), Or(p, x**2 == 2), Not(p))
求解器
z3提供了不同的求解器。命令solve,在之前已经使用过了,它使用z3求解器API来实现的。它的实现可以在z3的发行版中的z3.py文件中找到。下列例子演示了基本的Solver API。
x = Int('x')
y = Int('y')
s = Solver()
print s
s.add(x > 10, y == x + 2)
print s
print "Solving constraints in the solver s ..."
print s.check()
print "Create a new scope..."
s.push()
s.add(y < 11)
print s
print "Solving updated set of constraints..."
print s.check()
print "Restoring state..."
s.pop()
print s
print "Solving restored set of constraints..."
print s.check()
命令Solver()创建了一个通用的求解器。约束条件可以用方法add来添加。我们称该约束条件已经在求解器中被断言了。check()方法来断言这些约束条件,返回结果sat表示有解,而unsat则表示无解。我们也可以说该系统断言的约束条件是不可行的。最后,求解器也可能无法求解某约束系统而返回一个未知结果。
在一些应用当中,我们想要探究一些共享约束条件的类似问题,我们可以使用命令push和POP来完成这件事。每一个求解器都维护一个断言栈,命令push创建一个新的空间来保存当前栈大小,命令pop删除与匹配push命令之间的所有断言,check方法操作的对象总是对当前的断言栈。
以下展示了一个z3无法求解的例子,s.check()在这种情况下返回unknown。回想一下,Z3可以解决非线性的多项式约束,但2 ** x不是多项式
x = Real('x')
s = Solver()
s.add(2**x == 3)
print s.check()
以下示例显示如何遍历求解器的约束条件,以及如何收集check方法的性能统计信息
x = Real('x')
y = Real('y')
s = Solver()
s.add(x > 1, y > 1, Or(x + y > 3, x - y < 2))
print "asserted constraints..."
for c in s.assertions():
print c
print s.check()
print "statistics for the last check method..."
print s.statistics()
# Traversing statistics
for k, v in s.statistics():
print "%s : %s" % (k, v)
当z3找到断言的约束条件的解时,check命令返回sat。我们说z3满足了一系列约束条件,并称其解为一个模型(model),该模型满足断言的约束条件。模型是使每个断言约束成立的解释。 以下示例显示了检查模型的基本方法
x, y, z = Reals('x y z')
s = Solver()
s.add(x > 1, y > 1, x + y > 3, z - x < 10)
print s.check()
m = s.model()
print "x = %s" % m[x]
print "traversing model..."
for d in m.decls():
print "%s = %s" % (d.name(), m[d])
在上述例子中,函数Reals('x y, z')创建了x,y,z,3个变量。它是以下指令的简化版
x = Real('x')
y = Real('y')
z = Real('z')
表达式m[x]返回x在模型m中的解释,表达式"%s = %s" % (d.name(), m[d])返回一个字符串,其中第一个%s由d.name()代替,而第二个%s由m[d]来代替,在需要的时候,z3py自动将z3对象转换为文本表现形式。
算数
z3支持实数和整数变量,它们可以在某个问题中混合使用。和大多数程序语言一样,z3py在需要的时候会自动将整形表达式转换为实数表达式。以下例子展示了用不同方法声明整形变量和实数变量。
a, b, c = Ints('a b c')
d, e = Reals('d e')
solve(a > b + 2,
a == 2*c + 10,
c + b <= 1000,
d >= e)
命令simplify适用于z3表达式的简单转化。
x, y = Reals('x y')
# Put expression in sum-of-monomials form
t = simplify((x + y)**3, som=True)
print t
# Use power operator
t = simplify(t, mul_to_power=True)
print t
命令help_simplify()打印出所有可选操作。z3py允许用户使用两种风格来编写选项。z3内部选项名以":"开头,单词之间以"-"分开。这些选项也可用于z3py之中。z3py也支持类python的名字,这时"-"被删除,"-"被替换为"_",下面例子展示了这两种风格
x, y = Reals('x y')
# Using Z3 native option names
print simplify(x == y + 2, ':arith-lhs', True)
# Using Z3Py option names
print simplify(x == y + 2, arith_lhs=True)
print "\nAll available options:"
help_simplify()
z3py支持任意大的数字。下方的例子展示了如何用大数进行基本的算数运算。z3py只支持代数上的无理数,代数上的无理数足以表示多项式约束条件的解。z3py会将无理数转换为十进制小数的可读性强的形式,无理数的内部表现形式可以用sexpr()方法提取出来,它用数学公式的形式或类似lisp语言的表达式来展示z3的内部表现形式。
x, y = Reals('x y')
solve(x + 10000000000000000000000 == y, y > 20000000000000000)
print Sqrt(2) + Sqrt(3)
print simplify(Sqrt(2) + Sqrt(3))
print simplify(Sqrt(2) + Sqrt(3)).sexpr()
# The sexpr() method is available for any Z3 expression
print (x + Sqrt(y) * 2).sexpr()
机器运算
现代CPU和主流程序语言使用固定大小的位向量进行运算。在z3py中可以使用Bit-Vector来实现机器运算。它实现了无符号运算和有符号运算的精确定义。
下方例子展示了如何创建一个位向量变量和常量。函数BitVec('x', 16)创建了一个z3的位向量名为x,它的大小为16比特。为了方便,在z3py中整数常量可以用于创建位向量表达式。函数BitVecVal(10, 32)创建了一个大小为32比特的位向量,其数值为10。
x = BitVec('x', 16)
y = BitVec('y', 16)
print x + 2
# Internal representation
print (x + 2).sexpr()
# -1 is equal to 65535 for 16-bit integers
print simplify(x + y - 1)
# Creating bit-vector constants
a = BitVecVal(-1, 16)
b = BitVecVal(65535, 16)
print simplify(a == b)
a = BitVecVal(-1, 32)
b = BitVecVal(65535, 32)
# -1 is not equal to 65535 for 32-bit integers
print simplify(a == b)
在一些程序语言,比如C,C++,C#,Java,有符号整数和无符号整数在位向量的层面上没有任何区别。然而,z3为有符号数与无符号数提供了不同的运算操作版本。在z3py中,运算符<, <=, >, >=, /, %和>>用于有符号数,而无符号数对应的操作符是ULT, ULE, UGT, UGE, UDiv, URem 和 LShR。
# Create to bit-vectors of size 32
x, y = BitVecs('x y', 32)
solve(x + y == 2, x > 0, y > 0)
# Bit-wise operators
# & bit-wise and
# | bit-wise or
# ~ bit-wise not
solve(x & y == ~y)
solve(x < 0)
# using unsigned version of <
solve(ULT(x, 0))
运算符>>是算数右移操作,而<<是左移操作,而逻辑右移要使用LShR操作
函数
(有点译不动)
程序语言的函数中会有意外情况发生,比如异常抛出或从不返回,和这些程序语言不同,z3的函数中完全不会出现这种情况,这是因为所有的输入值都是已被定义的。z3是建立在一阶逻辑之上的。
对于一个约束条件,如x + y > 3,我们可以说x,y都是变量,在一些教材上,x和y被称作未有解释的量,也就是说,任何保证这个约束条件正确的解释都是允许的。
更精确的来说,函数和常量符号在一阶逻辑中都是未有解释或者说是自由的,即没有一种先天的解释赋予它们。这和属于签名理论的函数形成对比,比如+运算拥有固定且标准的解释,就是将两个数相加。这种自由的函数和常量拥有最大的灵活性。它们允许任何满足约束条件的解释。
为了说明未解释的函数和常量,让我们使用未解释的整数x,y,最后让f作为一个未被解释的函数,它接受一个整形参数并返回一个整形值。下方例子说明了如何强行出现一个解释
x = Int('x')
y = Int('y')
f = Function('f', IntSort(), IntSort())
solve(f(f(x)) == x, f(x) == y, x != y)
对f的求解为:f(0)为1,f(1)为0,f(c)为1,这里c为除0和1外任意数
在z3中,我们还可以在模型中为约束系统估计表达式。 以下示例显示如何使用evaluate方法
x = Int('x')
y = Int('y')
f = Function('f', IntSort(), IntSort())
s = Solver()
s.add(f(f(x)) == x, f(x) == y, x != y)
print s.check()
m = s.model()
print "f(f(x)) =", m.evaluate(f(f(x)))
print "f(x) =", m.evaluate(f(x))
Satisfiability and Validity
对于未解释符号进行适当的赋值能使得方程或约束条件结果总是为true,则称方程或约束条件F有效。以约束条件的结果始终为true作为条件,若存在适当的对未解释符号的赋值,则成约束条件可满足。有效性是关于找到一个命题的证明,而可满足性是找到约束条件的解(这里就很清楚了)。设想一个方程F包含a和b,我们在问F是否有效时,即在问对任意值的a和b来说,它是否总是为true。若F总是为true,则Not(F)则总是false,这时a和b不会有任何可满足的赋值组合,就称Not(F)是不可满足的,所以也可以这么说,当Not(F)是不可满足时,F是有效的。类似的,当Not(F)是无效的时,则F是可满足的。(注:有效性是通过验证来判断的,即给定数值进行判断,若任意判断都是无效的,则称不可满足。命题形式:对任意验证,若结果都是无效的,则称条件约束不可满足。逆否:若条件约束可以满足,则存在验证,使得结果有效)
p, q = Bools('p q')
demorgan = And(p, q) == Not(Or(Not(p), Not(q)))
print demorgan
def prove(f):
s = Solver()
s.add(Not(f))
if s.check() == unsat:
print "proved"
else:
print "failed to prove"
print "Proving demorgan..."
prove(demorgan)