-1

I want to use z3py to illustrate the following genealogy exercise (pa is “parent” and grpa is “grand-parent)

pa(Rob,Kev) ∧ pa(Rob,Sama) ∧ pa(Sama,Tho) ∧ pa(Dor,Jim) ∧ pa(Bor,Jim) ∧ pa(Bor,Eli) ∧ pa(Jim,Tho) ∧ pa(Sama,Samu) ∧ pa(Jim,Samu) ∧ pa(Zel,Max) ∧ pa(Samu,Max)

∀X,Y,Z pa(X,Z) ∧ pa(Z,Y) → grpa(X,Y)

The exercise consists in finding for which value of X one has the following:

∃X grpa(Rob,X) ∧ pa(X,Max)

(The answer being: for X == Samu.) I would like to rewrite this problem in z3py, so I introduce a new sort Hum (for “humans”) and write the following:

import z3

Hum = z3.DeclareSort('Hum')
pa = z3.Function('pa',Hum,Hum,z3.BoolSort())
grpa = z3.Function('grpa',Hum,Hum,z3.BoolSort())
Rob,Kev,Sama,Tho,Dor,Jim,Bor,Eli,Samu,Zel,Max = z3.Consts('Rob Kev Sama Tho Dor Jim Bor Eli Samu Zel Max', Hum)
s=z3.Solver()
for i,j in ((Rob,Kev),(Rob,Sama),(Sama,Tho),(Dor,Jim),(Bor,Jim),(Bor,Eli),(Jim,Tho),(Sama,Samu),(Jim,Samu),(Zel,Max),(Samu,Max)):
    s.add(pa(i,j))
x,y,z=z3.Consts('x y z',Hum)
whi=z3.Const('whi',Hum)
s.add(z3.ForAll([x,y,z],z3.Implies(z3.And(pa(x,z),pa(z,y)),grpa(x,y))))
s.add(z3.Exists(whi,z3.And(grpa(Rob,whi),pa(whi,Max))))

The code is accepted by Python and for

print(s.check())

I get

sat

Now I know there is a solution. The problem is: how do I get the value of whi?

When I ask for print(s.model()[whi]) I get None. When I ask for s.model().evaluate(whi) I get whi, which is not very helpful.

How can I get the information that whi must be Samu for the last formula to be true?

(Auxiliary question: why is there no difference between constants and variables? I'm a bit puzzled when I define x,y,z as constants although they are variable. Why can I not write x=Hum('x') to show that x is a variable of sort Hum?)

yannis
  • 819
  • 1
  • 9
  • 26

1 Answers1

1

When you write something like:

X, Y = Const('X Y', Hum)

It does not mean that you are declaring two constants named X and Y of sort Hum. (Yes, this is indeed confusing! Especially if you're coming from a Prolog like background!)

Instead, all it means is that you are saying there are two objects X and Y, which belong to the sort Hum. It does not even mean X and Y are different. They might very well be the same, unless you explicitly state it, like this:

s.assert(z3.Distinct([X, Y]))

This might also explain your confusion regarding constants and variables. In your model, everything is a variable; you haven't declared any constants at all.

Your question about how come whi is not Samu is a little trickier to explain, but it stems from the fact that all you have are variables and no constants at all. Furthermore, whi when used as a quantified variable will never have a value in the model: If you want a value for a variable, it has to be a top-level declared variable with its own assertions. This usually trips people who are new to z3py: When you do quantification over a variable, the top-level declaration is a mere trick just to get a name in the scope, it does not actually relate to the quantified variable. If you find this to be confusing, you're not alone: It's a "hack" that perhaps ended up being more confusing than helpful to newcomers. If you're interested, this is explained in detail here: https://theory.stanford.edu/~nikolaj/programmingz3.html#sec-quantifiers-and-lambda-binding But I'd recommend just taking it on faith that the bound variable whi and what you declared at the top level as whi are just two different variables. Once you get more familiar with how z3py works, you can look into the details and reasons behind this hack.

Coming back to your modeling question: You really want these constants to be present in your model. In particular, you want to say these are the humans in my universe and nobody else, and they are all distinct. (Kind of like Prolog's closed world assumption.) This sort of thing is done with a so-called enumeration sort in z3py. Here's how I would go about modeling your problem:

from z3 import *

# Declare an enumerated sort. In this declaration we create 'Human' to be a sort with
# only the elements as we list them below. They are guaranteed to be distinct, and further
# any element of this sort is guaranteed to be equal to one of these.
Human, (Rob, Kev, Sama, Tho, Dor, Jim, Bor, Eli, Samu, Zel, Max) \
   = EnumSort('Human', ('Rob', 'Kev', 'Sama', 'Tho', 'Dor', 'Jim', 'Bor', 'Eli', 'Samu', 'Zel', 'Max'))

# Uninterpreted functions for parent/grandParent relationship.
parent      = Function('parent',      Human, Human, BoolSort())
grandParent = Function('grandParent', Human, Human, BoolSort())

s = Solver()

# An axiom about the parent and grandParent functions. Note that the variables
# x, y, and z are merely for the quantification reasons. They don't "live" in the
# same space when you see them at the top level or within a ForAll/Exists call.
x, y, z = Consts('x y z', Human)
s.add(ForAll([x, y, z], Implies(And(parent(x, z), parent(z, y)), grandParent(x, y))))

# Express known parenting facts. Note that unlike Prolog, we have to tell z3 that
# these are the only pairs of "parent"s available.
parents = [ (Rob, Kev), (Rob, Sama), (Sama, Tho),  (Dor, Jim)  \
          , (Bor, Jim), (Bor, Eli), (Jim, Tho),  (Sama, Samu)  \
          , (Jim, Samu), (Zel, Max), (Samu, Max)               \
          ]

s.add(ForAll([x, y], Implies(parent(x, y), Or([And(x==i, y == j) for (i, j) in parents]))))

# Find what makes Rob-Max belong to the grandParent relationship:
witness = Const('witness', Human)
s.add(grandParent(Rob, Max))
s.add(grandParent(Rob, witness))
s.add(parent(witness, Max))

# Let's see what witness we have:
print s.check()
m = s.model()
print m[witness]

For this, z3 says:

sat
Samu

which I believe is what you were trying to achieve.

Note that the Horn-logic of z3 can express such problems in a nicer way. For that see here: https://rise4fun.com/Z3/tutorialcontent/fixedpoints. It's an extension that z3 supports which isn't available in SMT solvers, making it more suitable for relational programming tasks.

Having said that, while it is indeed possible to express these sorts of relationships using an SMT solver, such problems are really not what SMT solvers are designed for. They are much more suitable for quantifier-free fragments of logics that involve arithmetic, bit-vectors, arrays, uninterpreted-functions, floating-point numbers, etc. It's always fun to try these sorts of problems as a learning exercise, but if this sort of problem is what you really care about, you should really stick to Prolog and its variants which are much more suited for this kind of modeling.

alias
  • 28,120
  • 2
  • 23
  • 40
  • Dear alias (I wonder if that is your real name), thank you so much for this illuminating answer. If you don't mind I would like to profit a bit from your generosity with another two tiny questions: (1) when I write `S = DeclareSort('S') x = Const('x', S) s = Solver() s.add(ForAll(x, x == x)) print(s.check()) print(s.model())` I get the answer `sat`(satisfiable, that's natural) and `[]` Why do I get an empty array as answer? Is it because my domain is empty since I haven't defined any constant? (2)I often see this expression: `g = [else -> True]` is the meaning of `else` that – yannis Feb 23 '20 at 10:05
  • there is no explicit assignment and therefore all "other" values (the `else` case) are sent to `True`? – yannis Feb 23 '20 at 10:08
  • Remember that the `x` in `x = Const('x', S)` and the `x` in `Forall([x], x == x)` are completely different and have nothing to do with each other. It's only needed so you can write the `ForAll` and as I mentioned this is simply a hack to get z3py working. There are no empty domains in SMTLib. You can also get `[]` as a model if there are no constraints to solve, so nothing has propagated; but you can avoid that by using `evaluate`, which can print `x` since there are no constraints on it. Also: comments are usually not good for new questions in stack-overflow, it's best to ask a new question. – alias Feb 23 '20 at 16:58