You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
8.8 KiB

2 weeks ago
  1. Student 1: Name Surname Matriculation Number
  2. Student 2: Name Surname Matriculation Number
  3. ## Basics of z3
  4. The exercises you will solve in the practicals are only going to cover a small subset of the possibilities of solving problems with z3. If you are interested in more background or need to look into some details we suggest you to take a look [here](https://theory.stanford.edu/~nikolaj/programmingz3.html).
  5. A very small example program could like like this:
  6. ``` python
  7. # coding: utf-8
  8. import os, sys
  9. from z3 import *
  10. # v-- internal z3 representation
  11. x = Bool('x')
  12. #^-- python variable
  13. # v-- internal z3 representation
  14. gamma = Bool('g') # possible, but not advisable
  15. #^-- python variable
  16. # Declare a solver with which we can do some work
  17. solver = Solver()
  18. p = Bool('p')
  19. qu = Bool('q')
  20. r = Bool('r')
  21. # p -> q, r = ~q, ~p or r
  22. # Add constraints
  23. solver.add(Implies(p,qu))
  24. solver.add(r == Not(qu))
  25. solver.add(Or(Not(p), r))
  26. # solver.add(r == q)
  27. res = solver.check()
  28. if res != sat:
  29. print("unsat")
  30. sys.exit(1)
  31. m = solver.model()
  32. for d in m.decls():
  33. print("%s -> %s" % (d, m[d]))
  34. ```
  35. #### A Simple Example
  36. In a python program we usually follow this workflow:
  37. - import `z3`,
  38. - declare needed variables of specific `Sort` (this is the word we use for types in z3),
  39. - declare a solver: `solver = Solver()` and
  40. - add constraints for the declared variables to the solver.
  41. - After adding all the constraints we tell the solver to try to `check()` for satisfiability and if the solver tells us that the model is satisfiable we may
  42. - print the model.
  43. We can now dissect the example program from above and take a look at which steps we have taken:
  44. At first we import z3 `from z3 import *`.
  45. We then need to declare variables:
  46. ``` python
  47. # v-- internal z3 representation
  48. x = Bool('x')
  49. #^-- python variable
  50. # v-- internal z3 representation
  51. gamma = Bool('g') # possible, but not advisable
  52. #^-- python variable
  53. ```
  54. Lets have a closer look: In z3 we declare variable of some kind of `Sort` just like you would declare variable of some type in other language. z3 has a `BoolSort`, `IntSort`, `RealSort` and some more. Our example from above only covers propositional logic so far so we have declared our variables of type `Bool`.
  55. We have to distinct between z3 variables and python variables. The code block above gives you the answer to this distinction and as the second examples tells you, you may give these two different names, but this is not advisable since it will probably only confuse you and others which need to read your code.
  56. In order to check for satisfiability we are going to need a solver: `solver = Solver()`.
  57. In the next step we will add some constraints to the solver:
  58. ``` python
  59. p = Bool('p')
  60. qu = Bool('qu')
  61. r = Bool('r')
  62. # p -> q, r = ~q, ~p or r
  63. # Add constraints
  64. solver.add(Implies(p,qu))
  65. solver.add(r == Not(qu))
  66. solver.add(Or(Not(p), r))
  67. ```
  68. Adding constraints is done with the solvers `add()` method. Remember that the constraints have to be expressed in prefix notation.
  69. At the very end we have to tell the solver to check whether our constraints are satisfiable, is they are not we simply exit the program:
  70. ``` python
  71. res = solver.check()
  72. if res != sat:
  73. print("unsat")
  74. sys.exit(1)
  75. ```
  76. Our example is satisfiable so in the end we print the model by asking the solver for the created model and print all of the variables which have an associated value in the created model:
  77. ``` python
  78. m = solver.model()
  79. for d in m.decls():
  80. print("%s -> %s" % (d, m[d]))
  81. > q -> True
  82. > p -> False
  83. > r -> False
  84. ```
  85. This can be done with `solver.model().decls()` which lists the assignments as a dictionary. You can also evaluate individual variables in your model:
  86. ``` python
  87. m = solver.model()
  88. print("qu: " + str(m.eval(qu)))
  89. print("p: " + str(m.eval(p)))
  90. print("r: " + str(m.eval(r)))
  91. > qu: True
  92. > p: False
  93. > r: False
  94. ```
  95. ### First Order Logic Types and Constraints
  96. So far we have only touched propositional logic, but z3 is an SMT-solver so lets expand our knowledge to use these funtionalities.
  97. ``` python
  98. from z3 import Solver, Int
  99. from z3 import sat as SAT
  100. x, y = Int('x'), Int("%s" % "y") # create integer variables
  101. solver = Solver() # create a solver
  102. solver.add(x < 6 * y) # assert x < 6y
  103. solver.add(x % 2 == 1) # assert x == 1 mod 2
  104. solver.add(sum([x,y]) == 42) # assert x + y = 42
  105. if solver.check() == SAT: # check if satisfiable
  106. m = solver.model() # retrieve the solution
  107. print(m[x] + m[y]) # print symbolic sum
  108. print(m.eval(x) + m.eval(y)) # use eval to print
  109. # hint: use m[x].as_long() to get python integers
  110. for d in m.decls():
  111. print("%s -> %d" % (d, m[d].as_long()))
  112. > 35 + 7
  113. > 35 + 7
  114. > x -> 35
  115. > y -> 7
  116. ```
  117. From the example above, you can see that creating z3 integer variables follows the same principle as for booleans.
  118. Python expressions are valid in constraints too, for example using a built-in function: `solver.add(sum([x,y]) == 42)`.
  119. ### Custom Datatypes and Sorts
  120. So far we have used z3's capabilities by using boolean or integer valued variables. This already gives us quite a powerful tool, but we want to extend this to be able to use our own custom structures and datatypes. A first approach is to use the `DataType` functionality.
  121. ``` python
  122. Colour = DataType("Colour")
  123. ```
  124. This will create a placeholder that contains constructors and accessors for our custom `Colour` variables.
  125. ``` python
  126. Colour.declare("green")
  127. Colour.declare("yellow")
  128. ColourSort = Colour.create()
  129. ```
  130. We have now defined two constructors for possible values of our `Colour` variable type and finalized the definition of `Colour`. `.create()` returns a sort that we can now work with. z3 will now internally work with these possible values for `Colour`. You may think of `Colour` in the same way as of the `IntSort` mentioned above. Let's consider this once more. We have used `Int(...)` to tell z3 that we want it to create an internal representation of an integer variable. This could be refactored as such:
  131. ``` python
  132. x, y = Const('x', IntSort()), Const("%s" % "y", IntSort()) # create integer variables
  133. ```
  134. This means that `Int("x")` is only syntactic sugar to make our code more legible. But this also tells us how to use our `Colour` datatype:
  135. ``` python
  136. x = Const("cell", ColourSort)
  137. ```
  138. We have used the `DataType` functionality solely to model an enum-type variable. A constructor for such a datatype but might also have some accessor associated with it, allowing us to create algebraic structures like lists or trees.
  139. Another type of a custom structures are uninterpreted sorts. These can be created using `DeclareSort(...)`:
  140. ```python
  141. A = DeclareSort('A')
  142. x, y = Consts('x y', A)
  143. ```
  144. As you can see we may use them in a similar way to the above discussed `DataType`s. z3 will see `x` and `y` as of type `A`. Since these sorts are uninterpreted the do not come with any kind of semantics, i.e. we have no means to compare `x` and `y`. This will be useful for our next topic: Uninterpreted functions.
  145. Note that you do not have to actually `create()` your custom sort, it will be handled like a set of its declared variables.
  146. ### Uninterpreted Functions
  147. Uninterpreted functions give us a way to let z3 model relationships, equalities, etc. between certain variables. A function maps from a set of sorts to a sort.
  148. Consider this example (taken from [here](https://ece.uwaterloo.ca/~agurfink/ece653w17/z3py-advanced)):
  149. ```python
  150. from z3 import *
  151. A = DeclareSort('A')
  152. x, y = Consts('x y', A)
  153. f = Function('f', A, A)
  154. s = Solver()
  155. s.add(f(x) == y, f(f(x)) == x, x != y)
  156. s.check()
  157. m = s.model()
  158. print(m)
  159. print("interpretation assigned to A:")
  160. print("f(x) = " + m.evaluate(f(x)).decl().name())
  161. print("f(y) = " + m.evaluate(f(y)).decl().name())
  162. ```
  163. We use an uninterpreted sort `A` with values `x` and `y`. `f` is declared as a `Function(...)` mapping `A` to `A`. We are then telling the solver that `f` used on `x` will map to `y`, `f` used twice on `x` will give `x` again and that those two values are different. Checking for satisfiability will now check if such a function can exist. If it can the model produced by z3 will contain the look-up table for `f` that we have expected.
  164. ```python
  165. [x = A!val!0,
  166. y = A!val!1,
  167. f = [A!val!1 -> A!val!0, else -> A!val!1]]
  168. interpretation assigned to A:
  169. f(x) = A!val!1
  170. f(y) = A!val!0
  171. ```
  172. This function does not need to be fully defined, as z3 will only check if it can exist with respect to our expressed constraints. In order to get an assignment for all possible values in our sort, we can evaluate the model using the `model_completion=True` flag. This is taked from the `seating-arrangement` example:
  173. ```python
  174. arrangement = ["" for guest in range(len(guests))]
  175. for guest in guests:
  176. arrangement[m.evaluate(position(guest),model_completion=True).as_long()] = guest.decl().name()
  177. ```