-
Notifications
You must be signed in to change notification settings - Fork 30
/
tutorial.py
298 lines (242 loc) · 8.81 KB
/
tutorial.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# MonoSAT Python Tutorial
#This is a brief introduction to MonoSAT's Z3-inspired Python 3 library, which you can use to
#conveniently construct and solve formulas with MonoSAT.
#Before going any further, see the installation instructions for the Python library in [README].
#Also, be warned that this library has only been tested with Python 3.3+, and may not work on earlier
#versions (in particular, Python 2 may not work at all).
#Using MonoSAT in Python is as simple as:
#Import the MonoSAT library
from monosat import *
#Create two Boolean variables:
a = Var()
b = Var()
# c is true if a is true or b is false, and false otherwise
c = Or(a, Not(b))
#Add a unit clause to the solver, asserting that variable c must be true
Assert(c)
#Solve the instance in MonoSAT, returning either True if the instance is SAT, and False if it is UNSAT
result = Solve()
if result:
print("SAT")
#After a satisfiable call to Solve(), you can query the assignments given by the solver to
#individual variables using v.value()
print("a: " + str(a.value()))
print("b: " + str(b.value()))
print("c: " + str(c.value()))
else:
print("UNSAT")
# After a solve call, you can continue making further assertions, creating new variables,
# and making incremental calls to the solver
d= Var()
Assert(Implies(d, Or(a,b)))
# There are also assertion forms for the common logic constructions, which are slightly more efficient than creating a
# new literal and asserting it to true. An equivalent way to accomplish the above would have been:
AssertImplies(d, Or(a,b))
# Note that d does not yet have an assignment in the solver, and so d.value() will return None until the next solve call
print("Variable 'd' is unassigned, and so has value " + str(d.value()))
result = Solve()
if result:
print("SAT")
print("a: " + str(a.value()))
print("b: " + str(b.value()))
print("c: " + str(c.value()))
print("d: " + str(d.value())) # now d is assigned
else:
print("UNSAT")
#You can use the '~' operator to apply negation, the same way as Not()
Assert(~(And(a, b)))
result = Solve()
if result:
print("SAT")
print("a: " + str(a.value()))
print("b: " + str(b.value()))
print("c: " + str(c.value()))
print("d: " + str(d.value()))
else:
print("UNSAT")
#There is no way to remove assertions from MonoSAT yet, however, you can use assumptions to
#temporarily assert that a variable must be true (or false):
result = Solve([b])
if result:
print("SAT")
print("a: " + str(a.value()))
print("b: " + str(b.value()))
print("c: " + str(c.value()))
print("d: " + str(d.value()))
else:
print("Temporarily UNSAT, under the assumption that 'b' is true")
#If in the previous call, MonoSAT was only UNSAT under an assumption, the solver can still be used in subsequent calls:
result = Solve([~b])
if result:
print("SAT")
print("a: " + str(a.value()))
print("b: " + str(b.value()))
print("c: " + str(c.value()))
print("d: " + str(d.value()))
else:
print("UNSAT (under the assumption that 'b' is False)")
### Theory Support
# Now, onto the interesting stuff.
# In addition to Boolean logic, MonoSAT supports an extensive theory of finite graphs, including
# support for many common graph predicates such as reachability, shortest paths, maximum flows, acyclicity, and
# minimum spanning trees.
# MonoSAT also has support for BitVectors and Cardinality/Pseudo-Boolean constraints.
#Constructing a graph in MonoSAT is as easy as:
g = Graph()
#Create three nodes
n0 = g.addNode()
n1 = g.addNode()
n2 = g.addNode()
#Add three directed edges to the graph.
#You can also create undirected edges, using g.addUndirectedEdge().
e0 = g.addEdge(n0,n1)
e1 = g.addEdge(n1,n2)
e2 = g.addEdge(n0,n2)
#e0, e1, and e2 are *symbolic edges*, meaning that the edge (n0,n1) is included in G if and only if the
#theory atom e0 is assigned to True by MonoSAT.
#You can use e0,e1, and e2 just like variables in MonoSAT, and in that way control which edges are in the graph
#using arbitrary Boolean logic:
AssertNand(e0,e1,e2) # This is logically equivalent to Assert(Not(And(e0,e1,e2)))
AssertOr(e0,e2)
#You can even mix these symbolic edge variables with other logic from MonoSAT
AssertImplies(c, e0)
#Once you have created a graph and some edges, you can assert graph properties about that graph.
#For example, you can assert that node n2 must be reachable from node n0, in g
Assert(g.reaches(n0,n2))
result = Solve()
if result:
print("SAT")
print("e0: " + str(e0.value()))
print("e1: " + str(e1.value()))
print("e2: " + str(e2.value()))
else:
print("UNSAT")
#Graph predicates are 'double sided', so you can also assert that they are false, in order to
#prevent one node from reaching another:
Assert(Not(g.reaches(n1,n0)))
#You can also mix graph predicates in with arbitrary logic, just like variables and edges
Assert(Or(~b, ~g.reaches(n0,n1)))
result = Solve()
if result:
print("SAT")
print("e0: " + str(e0.value()))
print("e1: " + str(e1.value()))
print("e2: " + str(e2.value()))
else:
print("UNSAT")
#Edges can also have weights, represented as fixed-width, bounded bitvectors.
#(By bounded bitvectors, we mean that every bitvector in MonoSAT is asserted to
#be in the range [0, Max], and can never overflow/underflow)
#create a bitvector of width 4
bv0 = BitVector(4)
bv1 = BitVector(4)
bv2 = BitVector(4)
# BitVectors support addition, subtraction, and comparisons, but do not yet directly support
# negative values (the bitvectors are unsigned).
Assert(bv0+bv1 <= 7)
Assert(bv0 + bv2 >= bv1)
Assert(bv0 >= 2)
result = Solve()
if result:
print("SAT")
print("bv0: " + str(bv0.value()))
print("bv1: " + str(bv1.value()))
print("bv2: " + str(bv2.value()))
else:
print("UNSAT")
#When creating an edge, you can use bitvectors (or Python ints) as edge weights (otherwise, by default, every edge has weight '1'):
#Create a new graph
g2 = Graph()
#Create three nodes
n4 = g2.addNode()
n5 = g2.addNode()
n6 = g2.addNode()
#Add three weighted edges to the graph
#Weights may be bitvectors, or integer constants.
e3 = g2.addEdge(n4,n5, bv0)
e4 = g2.addEdge(n5,n6, bv1)
e5 = g2.addEdge(n4,n6, bv2)
#MonoSAT supports several useful graph predicates in addition to reachability, including:
#Shortest path constraints:
#Assert that the distance from n0 to n2 is less or equal to 3 (edges have default weights of 1)
Assert(g2.distance_leq(n4,n6,3))
#You can also use BitVectors in the arguments of graph predicates:
bv3 = BitVector(4)
Assert(Not(g2.distance_lt(n4,n6,bv3)))
Assert(bv3 == (bv0 + bv1))
result = Solve()
if result:
print("SAT")
print("e3: " + str(e3.value()))
print("e4: " + str(e4.value()))
print("e5: " + str(e5.value()))
print("bv0: " + str(bv0.value()))
print("bv1: " + str(bv1.value()))
print("bv2: " + str(bv2.value()))
print("bv3: " + str(bv3.value()))
else:
print("UNSAT")
#MonoSAT also features highly optimized support for maximum flow constraints, allowing for comparisons against either a python integer, or a bitvector:
Assert(g2.maxFlowGreaterOrEqualTo(n4,n6,3))
bv4 = BitVector(4)
Assert(g2.maxFlowGreaterOrEqualTo(n4,n6,bv4))
#Just like with reachability and distance constraints, these maximum flow predicates are two sided
#so you can assert that the maximum flow must be less than a given bitvector, or you can include the
#maximum flow predicate as part of arbitrary Boolean logic
Assert(Or(~c,~g2.maxFlowGreaterOrEqualTo(n4,n6,bv4+1)))
result = Solve()
if result:
print("SAT")
print("e3: " + str(e3.value()))
print("e4: " + str(e4.value()))
print("e5: " + str(e5.value()))
print("bv0: " + str(bv0.value()))
print("bv1: " + str(bv1.value()))
print("bv2: " + str(bv2.value()))
print("bv4: " + str(bv4.value()))
else:
print("UNSAT")
result = Solve([bv4==4])
if result:
print("SAT")
print("e3: " + str(e3.value()))
print("e4: " + str(e4.value()))
print("e5: " + str(e5.value()))
print("bv0: " + str(bv0.value()))
print("bv1: " + str(bv1.value()))
print("bv2: " + str(bv2.value()))
print("bv4: " + str(bv4.value()))
else:
print("UNSAT")
result = Solve([bv4>4, bv4<7])
if result:
print("SAT")
print("e3: " + str(e3.value()))
print("e4: " + str(e4.value()))
print("e5: " + str(e5.value()))
print("bv0: " + str(bv0.value()))
print("bv1: " + str(bv1.value()))
print("bv2: " + str(bv2.value()))
print("bv4: " + str(bv4.value()))
else:
print("UNSAT")
#MonoSAT also features good support for minimum spanning tree constraints (in undirected graphs):
g3 = Graph()
n7 = g3.addNode()
n8 = g3.addNode()
n9 = g3.addNode()
#Add three weighted, undirected edges to the graph
e6 = g3.addUndirectedEdge(n7,n8, 1)
e7 = g3.addUndirectedEdge(n8,n9, 2)
e8 = g3.addUndirectedEdge(n7,n9, 4)
Assert(g3.minimumSpanningTreeLessEq(3))
Assert(~g3.minimumSpanningTreeLessEq(1))
result = Solve()
if result:
print("SAT")
print("e6: " + str(e6.value()))
print("e7: " + str(e7.value()))
print("e8: " + str(e8.value()))
else:
print("UNSAT")
#(Minimum spanning tree constraints don't support bitvectors yet, but they could in the future)