-
Notifications
You must be signed in to change notification settings - Fork 0
/
SAT_solver_VSIDS_notimproved.py
447 lines (376 loc) · 16.2 KB
/
SAT_solver_VSIDS_notimproved.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
from SAT_defs import *
import copy
import pdb
import random
class SAT_solver_VSIDS:
def __init__(self, CNF_SAT_Problem):
self.CNF_SAT_Problem = CNF_SAT_Problem
# learned_clause keeps track of the last learned clause
self.learned_clause = []
self.Assignments = [None] * CNF_SAT_Problem.N_Vars
self.Guesses = [] # List of literals representing assignments made in branches
self.DEBUG = False
self.ASK = False
self.Unsolvable = False
self.Pre_Simplify()
# We don't need to calculate the heuristic on every branch,
# just update it
self.persistent_heuristic = [0] * self.CNF_SAT_Problem.N_Vars
# A counter for the variable phase, so that we can
# choose which assignment to try in a more intelligent
# manner than just doing false all the time
self.var_phase = [0] * self.CNF_SAT_Problem.N_Vars
# Tunables
self.DEBUG = False
self.ASK = False
self.NEW_CLAUSE_WEIGHT = 0.05 # New clauses are weighted by 1+x% more than old ones
self.INIT_HEUR = 0.0009765625 # We start low to better use a float's dynamic range
self.LIMIT_LEARN_CLAUSE_LEN = True # Significant speedups
self.MAX_LEARN_CLAUSE_LEN = 4 # 4 seems optimal
self.TRY_RANDOM_CHOICE = False # Does not seem to help currently
self.RANDOM_CHOICE_LKHD = 2 # Given in units compatible with the range
self.RANDOM_RANGE_START = 1
self.RANDOM_RANGE_END = 100
# Tunable init
self.curr_heur_val = self.INIT_HEUR
self.heur_factor = 1 + self.NEW_CLAUSE_WEIGHT
self.Init_Heur()
# Applies a version of the DPLL algorithm to solve the problem
# Returns False if impossible and True if solved
def Solve(self):
while True:
if self.DEBUG:
print('Current problem complexity: %d' %
complexity(self.CNF_SAT_Problem))
if self.ASK:
print('Press enter to continue')
x = input()
else:
x = ''
print()
if not self.Simplify():
if not self.Guesses: # Kore de saigo da (This ends here)
return False
if self.DEBUG:
print('Assignment is inconsistent, backtracking...')
if self.BackTrack():
continue
else:
return False
if self.DEBUG:
print('Problem complexity after simplification: %d' %
complexity(self.CNF_SAT_Problem))
if 'p' in x:
print(self.CNF_SAT_Problem)
if 'a' in x:
ass_print(self.Assignments)
# if('c' in x):
# self.DEBUG = False;
if 'n' in x:
self.ASK = False
# if('d' in x):
if 'c' in x:
pdb.set_trace()
if not self.CNF_SAT_Problem.Clauses:
return True # Problem Solved
if self.Assign_Units():
continue
if self.Assign_Pure():
continue
# Everything Failed. Branching
# At this point, the heuristic is updated and used to determine the guess to make
#
# We're currently using a VSIDS, described below
new_guess = Guess()
new_guess.Sentence_Before = copy.copy(self.CNF_SAT_Problem)
new_guess.Assignments_Before = copy.copy(self.Assignments)
# Update heuristic
chosen_lit = self.Update_Heur()
if(self.DEBUG):
print('Heuristic: %s' % [h for h in self.persistent_heuristic])
if (self.Assignments[chosen_lit] == None):
new_guess.Lit_ID = chosen_lit
if self.var_phase[chosen_lit] < 1:
new_guess.Tried[False] = True
self.Assignments[chosen_lit] = False
else:
new_guess.Tried[True] = True
self.Assignments[chosen_lit] = True
else:
# This really shouldn't happen
raise(ValueError("Tried to assign to already-assigned literal!"))
# Find the first unassigned
# for i in range(self.CNF_SAT_Problem.N_Vars):
# if(self.Assignments[i] == None):
# new_guess.Lit_ID = i;
# new_guess.Tried[False] = True;
# self.Assignments[i] = False;
# break;
self.Guesses.append(new_guess)
if(self.DEBUG):
print('No units or literals found - branching: %s' % new_guess)
print('Full tree:%s' % self.Guesses)
# Removes clauses with true literals
# Removes literals that are false
# Returns False if problem is unsolvable, and True otherwise
def Simplify(self):
if(self.Unsolvable):
return False
rem_clauses = []
for Clause in self.CNF_SAT_Problem.Clauses:
rem_lits = []
for lit in Clause:
if(self.Assignments[lit.ID] == None):
continue
if(lit.Affirm == self.Assignments[lit.ID]):
rem_clauses.append(Clause)
rem_lits = []
break
if(lit.Affirm != self.Assignments[lit.ID]):
rem_lits.append(lit)
for rem_lit in rem_lits:
Clause.remove(rem_lit)
if(not Clause):
return False
for rem_clause in rem_clauses:
self.CNF_SAT_Problem.Clauses.remove(rem_clause)
return True
# Removes contradicting literals in a clause
# Removes repeating literals in a clause
# Returns True if problem was found to be impossible, and False otherwise
# Only needs to be ran once
# Of all the times it was used, it did nothing, however it is kept
# because it guarantees that each clause only has one of each kind
# of literals
def Pre_Simplify(self):
for Clause in self.CNF_SAT_Problem.Clauses:
finished = False
while(not finished):
finished = True
for lit in Clause:
eq_lits = [x for x in Clause if x.ID ==
lit.ID and (not x is lit)]
if(eq_lits):
finished = False
#~ pdb.set_trace()
delete_all_eqs = False
for eq_lit in eq_lits:
if(eq_lit.Affirm != lit.Affirm):
delete_all_eqs = True
break
for eq_lit in eq_lits:
Clause.remove(eq_lit)
if(delete_all_eqs):
Clause.remove(lit)
if(not Clause):
self.Unsolvable = True
return
# Assign all the literals in unit clauses.
# Returns True if any units were found or problem was found to be
# unsolvable and False otherwise
# If it finds the problem unsolvable, it sets the flag Unsolvable
def Assign_Units(self):
ret = False
Clauses_to_Remove = []
for Clause in self.CNF_SAT_Problem.Clauses:
if(len(Clause) == 1):
ret = True
unit_lit = Clause[0]
# WARNING: not None evaluates to True
if(self.Assignments[unit_lit.ID] == (not unit_lit.Affirm)):
self.Unsolvable = True
return True
pdb.set_trace()
self.Assignments[unit_lit.ID] = unit_lit.Affirm
Clauses_to_Remove.append(Clause)
if(self.DEBUG and bool(Clauses_to_Remove)):
print('Unit clauses found: %s' % Clauses_to_Remove)
for Clause_to_Remove in Clauses_to_Remove:
self.CNF_SAT_Problem.Clauses.remove(Clause_to_Remove)
return ret
# Look for pure symbols and assign them
#
# Returns false if no pure symbols are found
def Assign_Pure(self):
Literals = [Literal_Info() for i in range(self.CNF_SAT_Problem.N_Vars)]
for Clause in self.CNF_SAT_Problem.Clauses:
for lit in Clause:
if(Literals[lit.ID].PureAffirm == None):
Literals[lit.ID].PureAffirm = lit.Affirm
Literals[lit.ID].N_Appear += 1
continue
if(Literals[lit.ID].N_Appear < 0):
continue
if(lit.Affirm == Literals[lit.ID].PureAffirm):
Literals[lit.ID].N_Appear += 1
else:
Literals[lit.ID].N_Appear = -1
ret = False
for ind in range(self.CNF_SAT_Problem.N_Vars):
if(Literals[ind].N_Appear > 0):
self.Assignments[ind] = Literals[ind].PureAffirm
ret = True
if(self.DEBUG and ret):
S = 'Pure literals found: '
for ind in range(self.CNF_SAT_Problem.N_Vars):
if(Literals[ind].N_Appear > 0):
S += '%s ' % Literal(ind, Literals[ind].PureAffirm)
print(S)
return ret
# Calculate the initial heuristic for the problem
#
# Very similar to the update, minus some details
#
# No return
def Init_Heur(self):
# We're using a modified VSIDS heuristic:
# The number of clauses a variable appears in is counted.
# These counts are updated whenever clauses are learned.
# Newly-learned clauses are weighted with greater values than
# existing clauses, as an alternative to periodically dividing
# the existing heuristic list.
# When needed, the whole list is divided by 2^2^8 to keep the whole
# thing in reasonable double-precision float territory
#
# The variable chosen for assignment is determined by analysis
# of its appearances
# This method seems no better than just setting them to False
for clause in self.CNF_SAT_Problem.Clauses:
for lit in clause:
self.persistent_heuristic[lit.ID] += self.curr_heur_val
if lit.Affirm:
self.var_phase[lit.ID] += 1
else:
self.var_phase[lit.ID] -= 1
self.curr_heur_val = self.curr_heur_val * self.heur_factor
# Seed the RNG
random.seed()
return
# Update the heuristic and choose the next assignment to try
#
# Returns the ID for the literal to set
def Update_Heur(self):
# We're using a modified VSIDS heuristic:
# The number of clauses a variable appears in is counted.
# These counts are updated whenever clauses are learned.
# Newly-learned clauses are weighted with greater values than
# existing clauses, as an alternative to periodically dividing
# the existing heuristic list.
# When needed, the whole list is divided by 2^2^8 to keep the whole
# thing in reasonable double-precision float territory
#
# The variable chosen for assignment is determined by analysis
# of its appearances
# This method seems no better than just setting them to False
#
# Return value is the index for the next assignment to try
for clause in self.learned_clause:
for lit in clause:
self.persistent_heuristic[lit.ID] += self.curr_heur_val
if lit.Affirm:
self.var_phase[lit.ID] += 1
else:
self.var_phase[lit.ID] -= 1
if self.learned_clause:
# Remove the learned clause now that it's been accounted for
# Don't try this on an empty list...
self.learned_clause.pop()
self.curr_heur_val = self.curr_heur_val * self.heur_factor
# Keep things representable by doubles
if max(self.persistent_heuristic) > 2**512:
self.persistent_heuristic[:] = [
x / 2**256 for x in self.persistent_heuristic]
self.curr_heur_val = self.INIT_HEUR
# Occasionally try a random assignment
if self.TRY_RANDOM_CHOICE:
rnd_no = random.randint(1, 100)
if rnd_no < self.RANDOM_CHOICE_LKHD:
# Do a random assignment instead
rnd_no = random.randint(0, len(self.Assignments) - 1)
# Check that the chosen variable is unassigned
while self.Assignments[rnd_no] != None:
rnd_no = random.randint(0, len(self.Assignments) - 1)
return rnd_no
# Choose the greatest unassigned value
chosen_lit = 0
max_heur = self.persistent_heuristic[0]
for i in range(1, len(self.persistent_heuristic)):
if self.persistent_heuristic[i] > max_heur:
if self.Assignments[i] == None:
max_heur = self.persistent_heuristic[i]
chosen_lit = i
return chosen_lit
# Backs up until the last guess that still has an untried option
#
# Returns false if no guesses were made (problem is UNSAT
# #)
def BackTrack(self):
# Start by learning the conflict clause
if self.LIMIT_LEARN_CLAUSE_LEN:
if len(self.Assignments) < self.MAX_LEARN_CLAUSE_LEN:
new_clause = []
for i in self.Guesses:
lit_val = self.Assignments[i.Lit_ID]
lit_i = Literal(i.Lit_ID, lit_val)
new_clause.append(lit_i)
else:
new_clause = []
for i in self.Guesses:
lit_val = self.Assignments[i.Lit_ID]
lit_i = Literal(i.Lit_ID, lit_val)
new_clause.append(lit_i)
# Insert the new clause into the problem and into the learned
# clauses list
self.CNF_SAT_Problem.Clauses.append(new_clause)
self.learned_clause.append(new_clause)
while(self.Guesses):
this_Guess = self.Guesses.pop()
# Determine which guess, if any, is left.
# If none are left, we pop another
Untried = [
a for a in this_Guess.Tried if this_Guess.Tried[a] == False]
if(Untried):
Untried = Untried[0]
this_Guess.Tried[Untried] = True
self.Assignments = this_Guess.Assignments_Before
self.Assignments[this_Guess.Lit_ID] = bool(Untried)
self.CNF_SAT_Problem = this_Guess.Sentence_Before
self.Guesses.append(this_Guess)
self.Unsolvable = False
if(self.DEBUG):
print('Guesses list: %s' % self.Guesses)
print('Complexity of recovered state: %d' %
complexity(self.CNF_SAT_Problem))
return True
#~ else:
#~ self.Assignments[this_Guess.Lit_ID] = None;
#~ continue;
return False
# Useful class to keep general info on the literals
class Literal_Info:
N_Appear = 0 # Num of clauses it appears in. Negative if impure
PureAffirm = None # This pure literal has which affirm
# For debugging:
def __repr__(self):
if(self.N_Appear < 0):
return '<Impure>'
else:
return '<%s,%d>' % (self.PureAffirm, self.N_Appear)
# Class that maintains info on guesses made
class Guess:
def __init__(self):
self.Lit_ID = -1
self.Tried = [False] * 2
self.Sentence_Before = SAT_Sentence()
self.Assignments_Before = []
# For debugging:
def __repr__(self):
return '<%d, %s>' % (self.Lit_ID + 1, self.Tried)
def ass_print(assignments):
ass_L = len(assignments)
L = ass_L // 10
if(ass_L % 10):
L += 1
for i in range(L):
print('(%d-%d) - %s' % (i * 10 + 1, min(i * 10 + 10, ass_L),
assignments[i * 10:min(i * 10 + 10, ass_L)]))