z3smtz3pyquantifiers

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


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.


Solution

  • 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.