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.

252 lines
8.1 KiB

1 week 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 Simple Example
  6. A typical workflow that integrates z3 into a python script follows these steps:
  7. - import `z3`,
  8. - declare needed variables of specific `Sort` (this is the word we use for types in z3),
  9. - declare a solver: `solver = Solver()` and
  10. - add constraints for the declared variables to the solver.
  11. - 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
  12. - print the model.
  13. Consider the following simple example:
  14. ``` python
  15. # coding: utf-8
  16. import os, sys
  17. from z3 import *
  18. # v-- internal z3 representation
  19. x = Bool('x')
  20. #^-- python variable
  21. # v-- internal z3 representation
  22. gamma = Bool('g') # possible, but not advisable
  23. #^-- python variable
  24. # Declare a solver with which we can do some work
  25. solver = Solver()
  26. p = Bool('p')
  27. qu = Bool('q')
  28. r = Bool('r')
  29. # p -> q, r = ~q, ~p or r
  30. # Add constraints
  31. solver.add(Implies(p,qu))
  32. solver.add(r == Not(qu))
  33. solver.add(Or(Not(p), r))
  34. # solver.add(r == q)
  35. res = solver.check()
  36. if res != sat:
  37. print("unsat")
  38. sys.exit(1)
  39. m = solver.model()
  40. for d in m.decls():
  41. print("%s -> %s" % (d, m[d]))
  42. ```
  43. At first we import z3 `from z3 import *`.
  44. We then need to declare variables:
  45. ``` python
  46. # v-- internal z3 representation
  47. x = Bool('x')
  48. #^-- python variable
  49. # v-- internal z3 representation
  50. gamma = Bool('g') # possible, but not advisable
  51. #^-- python variable
  52. ```
  53. Lets have a closer look: In z3 we declare variables of a `Sort`. In z3, you may use `BoolSort`, `IntSort`, `RealSort` and others. The example above only covers propositional logic, hence only uses variables declared as `Bool`.
  54. In order to check for satisfiability we are going to need a solver: `solver = Solver()`.
  55. In the next step we will add some constraints to the solver:
  56. ``` python
  57. p = Bool('p')
  58. qu = Bool('qu')
  59. r = Bool('r')
  60. # p -> q, r = ~q, ~p or r
  61. # Add constraints
  62. solver.add(Implies(p,qu))
  63. solver.add(r == Not(qu))
  64. solver.add(Or(Not(p), r))
  65. ```
  66. Adding constraints is done with the solvers `add()` method. These constraints are added to the solver as one conjunction.
  67. Finally, we can as z3 to check for satisfiability:
  68. ``` python
  69. res = solver.check()
  70. if res != sat:
  71. print("unsat")
  72. sys.exit(1)
  73. ```
  74. Our example is satisfiable so we are able to print the assigned values for each variable in the model `m`:
  75. ``` python
  76. m = solver.model()
  77. for d in m.decls():
  78. print("%s -> %s" % (d, m[d]))
  79. > q -> True
  80. > p -> False
  81. > r -> False
  82. ```
  83. This can be done with `solver.model().decls()` which lists the assignments as a dictionary. You can also evaluate individual variables in your model:
  84. ``` python
  85. m = solver.model()
  86. print("qu: " + str(m.eval(qu)))
  87. print("p: " + str(m.eval(p)))
  88. print("r: " + str(m.eval(r)))
  89. > qu: True
  90. > p: False
  91. > r: False
  92. ```
  93. ### First Order Logic Types and Constraints
  94. So far we have only touched propositional logic, but z3 is an SMT-solver so lets expand our knowledge to use these funtionalities.
  95. ``` python
  96. from z3 import Solver, Int
  97. from z3 import sat as SAT
  98. x, y = Int('x'), Int("%s" % "y") # create integer variables
  99. solver = Solver() # create a solver
  100. solver.add(x < 6 * y) # assert x < 6y
  101. solver.add(x % 2 == 1) # assert x == 1 mod 2
  102. solver.add(sum([x,y]) == 42) # assert x + y = 42
  103. if solver.check() == SAT: # check if satisfiable
  104. m = solver.model() # retrieve the solution
  105. print(m[x] + m[y]) # print symbolic sum
  106. print(m.eval(x) + m.eval(y)) # use eval to print
  107. # hint: use m[x].as_long() to get python integers
  108. for d in m.decls():
  109. print("%s -> %d" % (d, m[d].as_long()))
  110. > 35 + 7
  111. > 35 + 7
  112. > x -> 35
  113. > y -> 7
  114. ```
  115. From the example above, you can see that creating z3 integer variables follows the same principle as for booleans.
  116. Python expressions are valid in constraints too, for example using a built-in function: `solver.add(sum([x,y]) == 42)`.
  117. ### Custom Datatypes and Sorts
  118. 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.
  119. ``` python
  120. Colour = DataType("Colour")
  121. ```
  122. This will create a placeholder that contains constructors and accessors for our custom `Colour` variables.
  123. ``` python
  124. Colour.declare("green")
  125. Colour.declare("yellow")
  126. ColourSort = Colour.create()
  127. ```
  128. 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:
  129. ``` python
  130. x, y = Const('x', IntSort()), Const("%s" % "y", IntSort()) # create integer variables
  131. ```
  132. 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:
  133. ``` python
  134. x = Const("cell", ColourSort)
  135. ```
  136. 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.
  137. Another type of a custom structures are uninterpreted sorts. These can be created using `DeclareSort(...)`:
  138. ```python
  139. A = DeclareSort('A')
  140. x, y = Consts('x y', A)
  141. ```
  142. An uninterpreted sort may be used similarly as the above discussed `DataType`s. z3 will see `x` and `y` as of type `A`. Since these sorts are uninterpreted there are no semantics related to the variables. , e.g. we have no means to compare `x` and `y`.
  143. Note that you do not have to `create()` your custom sort, it will be handled like a set of its declared variables.
  144. ### Uninterpreted Functions
  145. Lastly, we cover uninterpreted functions that give us a way to model relationships, or mappings between variables. A function maps from a set of sorts to a sort.
  146. Consider this example (taken from [here](https://ece.uwaterloo.ca/~agurfink/ece653w17/z3py-advanced)):
  147. ```python
  148. from z3 import *
  149. A = DeclareSort('A')
  150. x, y = Consts('x y', A)
  151. f = Function('f', A, A)
  152. s = Solver()
  153. s.add(f(x) == y, f(f(x)) == x, x != y)
  154. s.check()
  155. m = s.model()
  156. print(m)
  157. print("interpretation assigned to A:")
  158. print("f(x) = " + m.evaluate(f(x)).decl().name())
  159. print("f(y) = " + m.evaluate(f(y)).decl().name())
  160. ```
  161. We use an uninterpreted sort `A` with values `x` and `y`. `f` is declared as a `Function(...)` mapping `A` to `A`. The function `f` is then constrainted, such that `f(x)` maps to `y`, `f(f(x))` maps to `x` again and that `x` and `y` need to be different values. Checking for satisfiability will now check whether such a function can exist. If z3 can find such a function, it will represent the look-up table for `f` in the satisfying model:
  162. ```python
  163. [x = A!val!0,
  164. y = A!val!1,
  165. f = [A!val!1 -> A!val!0, else -> A!val!1]]
  166. interpretation assigned to A:
  167. f(x) = A!val!1
  168. f(y) = A!val!0
  169. ```
  170. 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 taken from the `seating-arrangement` example:
  171. ```python
  172. arrangement = ["" for guest in range(len(guests))]
  173. for guest in guests:
  174. arrangement[m.evaluate(position(guest),model_completion=True).as_long()] = guest.decl().name()
  175. ```