A Skolem model in Z3 should be unique, but is printing several (and repeated) - z3

I am testing how similar are "assignment"-like models and "Skolem-function"-like models in Z3.
Thus, I proposed an experiment: I will create a formula for which the unique model is y=2; and try to "imitate" this formula so that the (unique) model is a Skolem function f(x)=2. I did this by using ExistsForall quantification for the y=2 case and ForallExists quantification for the f(x)=2 case.
Thus, I first performed the following (note that the y is existentially quantified from the top-level declaration):
from z3 import *
x,y = Ints('x y')
ct_0 = (x >= 2)
ct_1 = (y > 1)
ct_2 = (y <= x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
print(s.check())
print(s.model())
for i in range(0, 5):
if s.check() == sat:
m = s.model()[y]
print(m)
s.add(And(y != m))
This code successfully prints out y=2 as a unique model (no matter we asked for 5 more). Now, I tried the same for f(x)=2 (note that there is no y):
skolem = Function('skolem', IntSort(), IntSort())
x = Int('x')
ct0 = (x >= 2)
ct1 = (skolem(x) > 1)
ct2 = (skolem(x) <= x)
phi1 = ForAll([x], Implies(ct0, And(ct1,ct2)))
s = Solver()
s.add(phi1)
for i in range(0, 5):
if s.check() == sat:
m = s.model()
print(m)
s.add(skolem(x) != i)
This prints:
[skolem = [else -> 2]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, 1)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, -1)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, -1)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, -1)]]
My question is: why is the y=2 unique, whereas we get several Skolem functions? In Skolem functions, we get (and repeatedly) some functions in which the antecedent of phi1 (i.e., (x >= 2)) is negated (e.g., x=0); but in models, we do not get stuff like x=0 implies y=1, we only get y=2 because that is the unique model that does not depend on x. In the same way, [skolem = [else -> 2]] should be the unique "Skolem model" that does not depend on x.

There's a fundamental difference between these two queries. In the first one, you're looking for a single y that acts as the value that satisfies the property. And indeed y == 2 is the only choice.
But when you have a skolem function, you have an infinite number of witnesses. The very first one is:
skolem(x) = 2
i.e., the function that maps everything to 2. (You're internally equating this to the model y=2 in the first problem, but that's misleading.)
But there are other functions too. Here's the second one:
skolem(x) = if 2 <= x then 2 else 1
You can convince yourself this is perfectly fine, since it does give you the skolem function that provides a valid value for y (i.e., 2), when the consequent matters. What it returns in the else case is immaterial. (i.e., when x < 2). And similarly, you can simply do different things when x < 2, giving you an infinite number of skolem functions that work. (Of course, the difference is not interesting, but different nonetheless.)
What you really are trying to say, I guess, is there's nothing "else" that's interesting. Unfortunately that's harder to automate, since it's hard to get a Python function back from a z3 model. But you can do it manually:
from z3 import *
skolem = Function('skolem', IntSort(), IntSort())
x = Int('x')
ct0 = (x >= 2)
ct1 = (skolem(x) > 1)
ct2 = (skolem(x) <= x)
phi1 = ForAll([x], Implies(ct0, And(ct1,ct2)))
s = Solver()
s.add(phi1)
print(s.check())
print(s.model())
# The above gives you the model [else -> 2], i.e., the function that maps everything to 2.
# Let's add a constraint that says we want something "different" in the interesting case of "x >= 2":
s.add(ForAll([x], Implies(x >= 2, skolem(x) != 2)))
print(s.check())
This prints:
sat
[skolem = [else -> 2]]
unsat
which attests to the uniqueness of the skolem-function in the "interesting" case.

Related

A model of a simple formula 'Exists([y],ForAll([x],Phi))' should be 'y=2' but Z3 it is returning '[]'

Note the following Z3-Py code:
x, y = Ints('x y')
negS0= (x >= 2)
s1 = (y > 1)
s2 = (y <= x)
s = Solver()
phi = Exists([y],ForAll([x], Implies(negS0, And(s1,s2))))
s.add(phi)
print(s.check())
print(s.model())
This prints:
sat
[]
My question is: why is the model empty? I mean, I think y=2 should be a model...
Note that the same result happens with x and y being Real.
z3 will not include any quantified variable (in your case neither y nor x) in its model. Note that you cannot put x in a model anyhow, because the formula is true for all x: That's the meaning of universal quantification. For the outer-most existentials (like your y), z3 can indeed print the model value for that, but it chooses not to do so since it can be confusing: Imagine you had a phi2, which also had an outer-most existential named y: How would you know which y it would be that it prints in the model?
So, z3 keeps things simple and simply prints the top-level declared variables in the model. And since a top-level declaration is equivalent to an outermost existential, you can simply drop it:
from z3 import *
x, y = Ints('x y')
negS0= (x >= 2)
s1 = (y > 1)
s2 = (y <= x)
s = Solver()
phi = ForAll([x], Implies(negS0, And(s1,s2)))
s.add(phi)
print(s.check())
print(s.model())
This prints:
sat
[y = 2]
like you predicted. Note that this y is unambiguous, since it's declared at the top-level. (Of course, you can redefine it to be something else due to the loosely typed nature of Python bindings and still get yourself confused, but that's a different discussion.)

How to output more than one Skolem functions in Z3(Py)

I am playing with Skolem functions in Z3-Py. In the following lines, I describe the Skolem function that satisfies the formula Forall x. Exists y. (x>=2) --> (y>1) /\ (y<=x) interpreted over integers:
x = Int('x')
skolem = Function('skolem', IntSort(), IntSort())
ct_0 = (x >= 2)
ct_1 = (skolem(x) > 1)
ct_2 = (skolem(x) <= x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
print(s.check())
print(s.model())
Which I understand that it outputs the unique 'model' possible: y=2 (i.e., f(x)=2). It is printed as follows:
sat
[skolem = [else -> 2]]
I have a preliminary question: why does the function use an else? I guess it comes from the fact that "it is only activated" if the antecedent (x>=2) holds. But which is the co-domain of the function if it is not the else branch (output 2) the one activated, but the if branch?
But now it comes my actual question. I solved the same formula, but interpreted over reals:
x = Real('x')
skolem = Function('skolem', RealSort(), RealSort())
ct_0 = (x >= 2.0)
ct_1 = (skolem(x) > 1.0)
ct_2 = (skolem(x) <= x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
print(s.check())
print(s.model())
And this outputs... the same Skolem function! Is this right?
In case it is, it is still curious, since it could have chosen many others; e.g., y=1.5. Then, I would like to ask for another Skolem function to Z3, so I proceed as if I was asking for 5 new models:
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
for i in range(0,5):
if s.check() == sat:
m = s.model()
print(m)
s.add(Not(m))
But I get the following error: Z3Exception: True, False or Z3 Boolean expression expected. Received [skolem = [else -> 2]] of type <class 'z3.z3.ModelRef'>. I mean, I see Z3 expects Boolean for the negation (which is obvious hehe), so how can I encode the fact that "I add the negation of this Skolem function into the formula", i.e., that I want to find another Skolem function?
In your loop, you're trying to add the negation of your model m; but that's not what Not(m) means. In fact, Not(m) is meaningless: Not operates on booleans, not models; which is the bizarre error you're getting.
To do what you're trying to achieve; simply assert that you want a different skolem function:
from z3 import *
x = Real('x')
skolem = Function('skolem', RealSort(), RealSort())
ct_0 = (x >= 2)
ct_1 = (skolem(x) > 1)
ct_2 = (skolem(x) <= 2)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
for i in range(0, 5):
if s.check() == sat:
m = s.model()
print(m)
s.add(skolem(x) != i)
This prints:
[skolem = [else -> 2]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, 1/2)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, 1/2)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 3/2, 1/2)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 3/2, 1/2)]]
Now this is a little hard to read, but I think you can figure it out. A function model is simply a finite-mapping with an else clause, which is what a finite function is. The Var call refers to its argument. I haven't looked at the definitions z3 provides in detail to make sure they're good, but I have no reason to suspect why not. See if you can work this out for yourself; it's a valuable exercise! Feel free to ask further questions if anything isn't clear.

"Intersection" of two Skolem functions is not as expected in Z3(Py)

Z3(Py) does not “intersect” Skolem functions as I expected.
I will try to explain my doubt using an imaginary problem which is the one that I have been testing on.
Consider a system that has two processes A and B, and an environment chooses between A or B. When process A is “activated”, then the system must solve P1: Exists y. Forall x. (x>=2) --> (y>1) /\ (y<=x). When the process B is “activated” then the system must solve P2: Exists y. Forall x. (x<2) --> (y>1) /\ (y>x). We can see, in Z3-PY, models of both the P1 and the P2 formulae:
#P1
x,y = Ints('x y')
ct_0 = (x >= 2)
ct_1 = (y > 1)
ct_2 = (y <= x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
print(s.check())
print(s.model())
#P2
x,y = Ints('x y')
ct_0 = (x < 2)
ct_1 = (y > 1)
ct_2 = (y > x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
print(s.check())
print(s.model())
Note that the model of P1 is unique: y=2; whereas a model of P2 is y=2, but also y=3, y=4, y=5, ... ad infinitum.
Then, I consider that the "intersection" of both P1 and P2 is the model y=2. In other words, whenever the environment chooses A or B, i.e., whenever the environment forces the system to solve P1 or P2, the system can just respond y=2 and will satisfy the formula, no matter what the environment chose.
I obtained this (unique) “intersection” y=2 by performing the (unique) model of the conjunction of both formulae:
#P1 and P2 intersection
#Note that, this time, I distinguish between ct_k and ctk because I executed them in the same script, no other reason involved.
x,y = Ints('x y')
ct_0 = (x >= 2)
ct_1 = (y > 1)
ct_2 = (y <= x)
phi0 = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
ct0 = (x < 2)
ct1 = (y > 1)
ct2 = (y > x)
phi1 = ForAll([x], Implies(ct0, And(ct1,ct2)))
phiT = And(phi0,phi1)
s = Solver()
s.add(phiT)
print(s.check())
print(s.model())
for i in range(0, 5):
if s.check() == sat:
m = s.model()[y]
print(m)
s.add(And(y != m))
This outputs [y = 2] as expected.
Now, consider I have a problem: P1 and P2 are no longer Exists-Forall formulae, but Forall-Exists formulae: this means that we will not have models of y, but Skolem functions (as solved in this question What does a model mean in a universally quantified formula? Is it a function?). I implement that as follows:
#P1 Skolem
x = Int('x')
skolem = Function('skolem', IntSort(), IntSort())
ct_0 = (x >= 2)
ct_1 = (skolem(x) > 1)
ct_2 = (skolem(x) <= x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
print(s.check())
print(s.model())
#P2 Skolem
x = Real('x')
skolem = Function('skolem', RealSort(), RealSort())
ct_0 = (x < 2)
ct_1 = (skolem(x) > 1)
ct_2 = (skolem(x) > x)
phi = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
s = Solver()
s.add(phi)
As we can see, both of them return [skolem = [else -> 2]] as the first option, and then they offer other (different) options, such as [x = 0, skolem = [else -> If(1 <= Var(0), 5, 2)]] for P2.
My question is: how can I perform the “intersection” of these two formulae? In general, how can I perform intersection of n skolem functions (in Z3)? I tried as follows (using the same idea as before when seeking y=2:
#P1 and P2 Skolem intersection
#Once again, I distinguish ct_k and ctk for execution reasons.
x,y = Ints('x y')
ct_0 = (x >= 2)
ct_1 = (skolem(x) > 1)
ct_2 = (skolem(x) <= x)
phi0 = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
ct0 = (x < 2)
ct1 = (skolem(x) > 1)
ct2 = (skolem(x) > x)
phi1 = ForAll([x], Implies(ct0, And(ct1,ct2)))
phiT = And(phi0,phi1)
s = Solver()
s.add(phiT)
print(s.check())
print(s.model())
for i in range(0, 5):
if s.check() == sat:
m = s.model()
print(m)
s.add(skolem(x) != i)
And results were as follows:
[skolem = [else -> 3/2]]
[skolem = [else -> 3/2]]
[x = 0, skolem = [else -> 2]]
[x = 0, skolem = [else -> 2]]
[x = 0, skolem = [else -> 3/2]]
[x = 0, skolem = [else -> 3/2]]
What does this mean? Why does it not output just [skolem = [else -> 2]]? In mean, in the same way that y=2 was the model with the other quantifier alternation.
Also, how does it offer 3/2 if we are in the domain of integers?
PS: In the Forall-Exists version of P1, if we print several Skolem functions, then the result is as follows:
[skolem = [else -> 2]]
[skolem = [else -> 2]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, 1)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, -1)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, -1)]]
[x = 0, skolem = [else -> If(2 <= Var(0), 2, -1)]]
How is this so?
In one of your programs, you have the declaration:
skolem = Function('skolem', RealSort(), RealSort())
I think you confused yourself by somehow leaving this in? It's hard to tell when you don't post code segments that are fully loadable by themselves. In any case, the following:
from z3 import *
skolem = Function('skolem', IntSort(), IntSort())
#P1 and P2 Skolem intersection
#Once again, I distinguish ct_k and ctk for execution reasons.
x,y = Ints('x y')
ct_0 = (x >= 2)
ct_1 = (skolem(x) > 1)
ct_2 = (skolem(x) <= x)
phi0 = ForAll([x], Implies(ct_0, And(ct_1,ct_2)))
ct0 = (x < 2)
ct1 = (skolem(x) > 1)
ct2 = (skolem(x) > x)
phi1 = ForAll([x], Implies(ct0, And(ct1,ct2)))
phiT = And(phi0,phi1)
s = Solver()
s.add(phiT)
for i in range(0, 5):
if s.check() == sat:
m = s.model()
print(m)
s.add(skolem(x) != i)
Prints:
[skolem = [else -> 2]]
[x = 0,
skolem = [else ->
If(And(1 <= Var(0), 2 <= Var(0)),
2,
If(1 <= Var(0), 4, 2))]]
[x = 0,
skolem = [else ->
If(And(1 <= Var(0), 2 <= Var(0)),
2,
If(1 <= Var(0), 4, 2))]]
[x = 0,
skolem = [else ->
If(And(1 <= Var(0), 2 <= Var(0)),
2,
If(1 <= Var(0), 5, 3))]]
[x = 0,
skolem = [else ->
If(And(1 <= Var(0), 2 <= Var(0)),
2,
If(1 <= Var(0), 6, 4))]]
which looks fine to me. Does this help resolve your issue?

Why is Z3 giving me unsat for the following formula?

I have the following formula and Python code trying to find the largest n satisfying some property P:
x, u, n, n2 = Ints('x u n n2')
def P(u):
return Implies(And(2 <= x, x <= u), And(x >= 1, x <= 10))
nIsLargest = ForAll(n2, Implies(P(n2), n2 <= n))
exp = ForAll(x, And(P(n), nIsLargest))
s = SolverFor("LIA")
s.reset()
s.add(exp)
print(s.check())
if s.check() == sat:
print(s.model())
My expectation was that it would return n=10, yet Z3 returns unsat. What am I missing?
You're using the optimization API incorrectly; and your question is a bit confusing since your predicate P has a free variable x: Obviously, the value that maximizes it will depend on both x and u.
Here's a simpler example that can get you started, showing how to use the API correctly:
from z3 import *
def P(x):
return And(x >= 1, x <= 10)
n = Int('n')
opt = Optimize()
opt.add(P(n))
maxN = opt.maximize(n)
r = opt.check()
print(r)
if r == sat:
print("maxN =", maxN.value())
This prints:
sat
maxN = 10
Hopefully you can take this example and extend it your use case.

Use Z3 to find counterexamples for a 'guess solution' to a particular CHC system?

Suppose I have the following CHC system (I(x) is an unkown predicate):
(x = 0) -> I(x)
(x < 5) /\ I(x) /\ (x' = x + 1) -> I(x')
I(x) /\ (x >= 5) -> x = 5
Now suppose I guess a solution for I(x) as x < 2 (actually an incorrect guess). Can I write code for Z3Py to (i) check if it is a valid solution and (ii) if it's incorrect find a counterexample, i.e. a value which satisfies x < 2 but does not satisfy atleast one of the above 3 equations? (For eg: x = 1, is a counterexample as it does not satisfy the 2nd equation?)
Sure. The way to do this sort of reasoning is to assert the negation of the conjunction of all your constraints, and ask z3 if it can satisfy it. If the negation comes back satisfiable, then you have a counter-example. If it is unsat, then you know that your invariant is good.
Here's one way to code this idea, in a generic way, parameterized by the constraint generator and the guessed invariant:
from z3 import *
def Check(mkConstraints, I):
s = Solver()
# Add the negation of the conjunction of constraints
s.add(Not(mkConstraints(I)))
r = s.check()
if r == sat:
print("Not a valid invariant. Counter-example:")
print(s.model())
elif r == unsat:
print("Invariant is valid")
else:
print("Solver said: %s" % r)
Given this, we can code up your particular case in a function:
def System(I):
x, xp = Ints('x xp')
# (x = 0) -> I(x)
c1 = Implies(x == 0, I(x))
# (x < 5) /\ I(x) /\ (x' = x + 1) -> I(x')
c2 = Implies(And(x < 5, I(x), xp == x+1), I(xp))
# I(x) /\ (x >= 5) -> x = 5
c3 = Implies(And(I(x), x >= 5), x == 5)
return And(c1, c2, c3)
Now we can query it:
Check(System, lambda x: x < 2)
The above prints:
Not a valid invariant. Counter-example:
[xp = 2, x = 1]
showing that x=1 violates the constraints. (You can code so that it tells you exactly which constraint is violated as well, but I digress.)
What happens if you provide a valid solution? Let's see:
Check(System, lambda x: x <= 5)
and this prints:
Invariant is valid
Note that we didn't need any quantifiers, as top-level variables act as existentials in z3 and all we needed to do was to find if there's an assignment that violated the constraints.

Resources