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.

311 lines
12 KiB

3 years ago
3 years ago
3 years ago
3 years ago
  1. **Under Construction!**
  2. # Logic and Computability - Practical Bonus Assignments 2022
  3. This repository will be used for submissions of *Logic and Computability* practical assignments in the summer semester 2022.
  4. ## Programming Exercises
  5. This year you will have the chance to solve up to four different programming exercises to achieve some bonus points. Our main goal is to get you acquainted with the tools and how to use them to solve interesting problems. To make things easier, we have prepared a skeleton for each programming exercise. We have already implemented most of the scaffolding code so that you only need to do the encoding into SMT. We have marked all of these locations in the code with short `# todo` descriptions, you should not need to modify anything else. There are also several hints throughout the skeleton where we suggest the best approach of doing the task. We marked them with `# hint`. Your code should not include any additional packages, and we will test them in an environment where only the SMT solver `z3` is available (apart for some colorizing packages, which will be listed in `requirements.txt`).
  6. We will provide you with the skeleton for the programming exercises so that you only have to do the SMT encoding and testing.
  7. 1. First, clone this repository and enter its root directory.
  8. ``` bash
  9. git clone https://git.pranger.xyz/sp/lub2022-practical.git --origin upstream
  10. ```
  11. 2. Start with solving the examples!
  12. We will provide you with your own repositories over the course of the next week. As soon as you have these repositories, we will give you a short description on how you can push your solutions.
  13. Your exercises will be tested! Make sure to write a few test cases for yourself.
  14. In general, we also provide you with at least one test case, and if it works correctly, you know that you did an excellent job and will probably get all points.
  15. ## Basics of Z3
  16. The exercises we will discuss in this lecture 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).
  17. ### Types, Sorts and Internal Representation
  18. First of all we have to cover the different syntax styles Z3 covers. Z3 facilitates the SMT-LIB standard (you can [read more here](http://smtlib.cs.uiowa.edu/language.shtml), but this is definitely not needed for this course) and we therefor need to use a **prefix** notation in our statements. This means that a statement like
  19. ```
  20. p or q
  21. ```
  22. needs to be written like such:
  23. ``` python
  24. Or(p,q)
  25. ```
  26. If you search the internet for examples you will probably stumble upon a syntax like this:
  27. ```
  28. (or p q)
  29. (implies r s)
  30. etc...
  31. ```
  32. which is just a different way of expressing the statement in prefix notation.
  33. The first example that we saw above can be used with the python library [z3-solver](https://pypi.org/project/z3-solver/) (make sure that you `pip install` this and not the `z3` package!).
  34. A very small example program could like like this:
  35. ``` python
  36. # coding: utf-8
  37. import os, sys
  38. from z3 import *
  39. # v-- internal Z3 representation
  40. x = Bool('x')
  41. #^-- python variable
  42. # v-- internal Z3 representation
  43. gamma = Bool('g') # possible, but not advisable
  44. #^-- python variable
  45. # Declare a solver with which we can do some work
  46. solver = Solver()
  47. p = Bool('p')
  48. qu = Bool('q')
  49. r = Bool('r')
  50. # p -> q, r = ~q, ~p or r
  51. # Add constraints
  52. solver.add(Implies(p,qu))
  53. solver.add(r == Not(qu))
  54. solver.add(Or(Not(p), r))
  55. # solver.add(r == q)
  56. res = solver.check()
  57. if res != sat:
  58. print("unsat")
  59. sys.exit(1)
  60. m = solver.model()
  61. for d in m.decls():
  62. print("%s -> %s" % (d, m[d]))
  63. ```
  64. #### Workflow
  65. In a python program we usually follow this workflow:
  66. - import `z3`,
  67. - declare needed variables of specific `Sort` (this is the word we use for types in z3),
  68. - declare a solver: `solver = Solver()` and
  69. - add constraints for the declared variables to the solver.
  70. - 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
  71. - print the model.
  72. We can now dissect the example program from above and take a look at which steps we have taken:
  73. At first we import z3 `from z3 import *`.
  74. We then need to declare variables:
  75. ``` python
  76. # v-- internal Z3 representation
  77. x = Bool('x')
  78. #^-- python variable
  79. # v-- internal Z3 representation
  80. gamma = Bool('g') # possible, but not advisable
  81. #^-- python variable
  82. ```
  83. 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`.
  84. 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.
  85. In order to check for satisfiability we are going to need a solver: `solver = Solver()`.
  86. In the next step we will add some constraints to the solver:
  87. ``` python
  88. p = Bool('p')
  89. qu = Bool('q')
  90. r = Bool('r')
  91. # p -> q, r = ~q, ~p or r
  92. # Add constraints
  93. solver.add(Implies(p,qu))
  94. solver.add(r == Not(qu))
  95. solver.add(Or(Not(p), r))
  96. ```
  97. Adding constraints is done with the solvers `add()` method. Remember that the constraints have to be expressed in prefix notation.
  98. 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:
  99. ``` python
  100. res = solver.check()
  101. if res != sat:
  102. print("unsat")
  103. sys.exit(1)
  104. ```
  105. 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:
  106. ``` python
  107. m = solver.model()
  108. for d in m.decls():
  109. print("%s -> %s" % (d, m[d]))
  110. > q -> True
  111. > p -> False
  112. > r -> False
  113. ```
  114. This can be done with `solver.model().decls()` which resembles a python dictionary. If you know your model is going to be small you may just simply evaluate each variable individually:
  115. ``` python
  116. m = solver.model()
  117. print("qu: " + str(m.eval(qu)))
  118. print("p: " + str(m.eval(p)))
  119. print("r: " + str(m.eval(r)))
  120. > qu: True
  121. > p: False
  122. > r: False
  123. ```
  124. *Note the difference between the naming of the internal representation of variables and the python variables.*
  125. ### First Order Logic Types and Constraints
  126. So far we have only touched propositional logic, but z3 is an SMT-solver so lets expand our knowledge to use these funtionalities.
  127. ``` python
  128. from z3 import Solver, Int
  129. from z3 import sat as SAT
  130. x, y = Int('x'), Int("%s" % "y") # create integer variables
  131. solver = Solver() # create a solver
  132. solver.add(x < 6 * y) # assert x < 6y
  133. solver.add(x % 2 == 1) # assert x == 1 mod 2
  134. solver.add(sum([x,y]) == 42) # assert x + y = 42
  135. if solver.check() == SAT: # check if satisfiable
  136. m = solver.model() # retrieve the solution
  137. print(m[x] + m[y]) # print symbolic sum
  138. print(m.eval(x) + m.eval(y)) # use eval to print
  139. # hint: use m[x].as_long() to get python integers
  140. for d in m.decls():
  141. print("%s -> %d" % (d, m[d].as_long()))
  142. > 35 + 7
  143. > 35 + 7
  144. > x -> 35
  145. > y -> 7
  146. ```
  147. We can tell from the example above that creating z3 integer variables follows the same principle as with booleans. Additionally we can tell that the strings that we pass allow typical python manipulations, so for example pythons `%`-formatting.
  148. Python expressions are valid in constraints too, for example using a built-in function: `solver.add(sum([x,y]) == 42)`.
  149. With this we can tell that using z3 and python interleaved is providing us with a powerful tool. We are going to discuss two simple examples together which will be linked in this README.
  150. #### Examples Discussion
  151. We have prepared two videos discussing the topics from above where we also discuss some additional example programs.
  152. - [Basics of Z3](https://www.youtube.com/watch?v=WRcLsvZObG0)
  153. - [Password Cracking](https://www.youtube.com/watch?v=hvEJzmCUmoI)
  154. ### Custom Datatypes and Sorts
  155. So far we have used Z3s 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.
  156. ``` python
  157. Colour = DataType("Colour")
  158. ```
  159. This will create a placeholder that contains constructors and accessors for our custom `Colour` variables.
  160. ``` python
  161. Colour.declare("green")
  162. Colour.declare("yellow")
  163. Colour = Colour.create()
  164. ```
  165. 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:
  166. ``` python
  167. x, y = Const('x', IntSort()), Const("%s" % "y", IntSort()) # create integer variables
  168. ```
  169. 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:
  170. ``` python
  171. x = Const("cell", Colour)
  172. ```
  173. 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.
  174. Another type of a custom structures are uninterpreted sorts. These can be created using `DeclareSort(...)`:
  175. ```python
  176. A = DeclareSort('A')
  177. x, y = Consts('x y', A)
  178. ```
  179. 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.
  180. Note that you do not have to actually `create()` your custom sort, it will be handled like a set of its declared variables.
  181. ### Uninterpreted Functions
  182. Uninterpreted functions give us a way to let Z3 solve for relationships, equalities, etc. between certain variables. A function, in the terminology of Z3, maps from a set of sort to a sort, just as the function `square: IntSort() -> IntSort()` could for example map all integers to their squared values we see functions as look-up tables, without the usual semantics that you would associate to them.
  183. Consider this example (taken from [here](https://ece.uwaterloo.ca/~agurfink/ece653w17/z3py-advanced)):
  184. ```python
  185. from z3 import *
  186. A = DeclareSort('A')
  187. x, y = Consts('x y', A)
  188. f = Function('f', A, A)
  189. s = Solver()
  190. s.add(f(x) == y, f(f(x)) == x, x != y)
  191. s.check()
  192. m = s.model()
  193. print(m)
  194. print("interpretation assigned to A:")
  195. print("f(x) = " + m.evaluate(f(x)).decl().name())
  196. print("f(y) = " + m.evaluate(f(y)).decl().name())
  197. ```
  198. 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.
  199. ```python
  200. [x = A!val!0,
  201. y = A!val!1,
  202. f = [A!val!1 -> A!val!0, else -> A!val!1]]
  203. interpretation assigned to A:
  204. f(x) = A!val!1
  205. f(y) = A!val!0
  206. ```
  207. 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 `ex4`:
  208. ```python
  209. arrangement = ["" for guest in range(len(guests))]
  210. for guest in guests:
  211. arrangement[m.evaluate(position(guest),model_completion=True).as_long()] = guest.decl().name()
  212. ```