-
Notifications
You must be signed in to change notification settings - Fork 4
/
SAT.py
1862 lines (1437 loc) · 82.6 KB
/
SAT.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
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# coding: utf-8
# # SAT Solver implemented using CDCL Algorithm
#
# SAT is the satisfiability problem wherein we are given some clauses containing propositional variables and the correspoding literals and we need to find whether the formula is satisfiable or unsatisfiable. A SAT Solver is a program that solves a SAT problem. Most of the state-of-the-art SAT solvers nowadays use the **CDCL (Conflict Driven Clause Learning)** algorithm to implement the solver.
#
# ## CDCL Approach
#
# To check whether the formula is SAT/UNSAT, the solver first picks a variable and assigns a boolean value to it. This is the **Decide** phase and is controlled by the decision heuristics used by the solver. Once the value is decided, the solver performs the **Boolean Constraint Propogation (BCP)** which is responsible for making implications after performing propogation once a variable is decided. Suppose we have a 3 literal clause (1,-2,3) and in the first 2 deciding steps, we set 1 as False and 2 as True. So, to make this clause true, we have to set 3 as true. This is basically what is intended by BCP. While the BCP makes implications, there can be 2 cases:
#
# 1. It can reach a conflict. A conflict occurs if the BCP wants to set a variable v to the boolean value b but v has already been assigned !b (not b) either during decide phase or the BCP phase. Once this conflict occurs, the solver analyzes the conflict (**Analyze Conflict**) phase.
# 1. If the conflict arises at level 0 (level 0 is the level where no decisions have been made and all the implications are the ones directly made using the input formula), it means that the given problem is UNSAT (unsatisfiable).
# 2. If the conflict is not at level 0, then the solver comes out with a conflict clause. This clause is such that it has to be true or unless this conflict will occur again. So, this clause is added to the initial set of clauses which prevents this conflict to occur again in the future. Once the conflict clause is decided, it is used to find the level (in the decision tree) to which the solver should **Backtrack** (backjump). In backtracking, the solver undoes all the assignments and implications made at the levels greater than the backtrack level and reaches the backtrack level with the conflict clause added. It starts BCP again and this continues until either there are no conflicts and we reach case 2 or there is a conflict at level 0 and we reach case 1A.
#
# 2. All the implications implied by the latest decision are made and the solver again goes to the Decide phase. If there are unassigned variables left, then the solver picks a variable and value to be assigned to it based on the heuristic and starts BCP. Else if there are no unassigned variables left, the problem is SAT (satisfiable) as all the variables have been assigned without any conflict.
#
# Though the above algorithm is correct, but while solving some bigger problems, the solver can get stuck in a bad part and can spend redundant time there. If it would have taken another branch, the problem could have been solved earlier. To take care of this, we introduce **Restarts** in the solver. Restart heuristics analyze when has the solver stuck and makes the solver restart its computation. In restart, the solver undoes all the decisions and implications made on levels greater than equal to 1. The values used by the decision heuristic and the learned clauses are kept as is and these now lead to different set of variables being decided by the solver (as the decision heuristics and clause set is different now). Restarts have been found very effective and are used in all the state-of-the-art solvers.
# <hr style="border:2px solid gray"> </hr>
# The code below will implement this CDCL Algorithm. The details of the phases are explained below. The code initially has the import statements and helper classes.
# In[1]:
import os
import sys
import time
import json
import random
from collections import OrderedDict
# Import the Priority Queue class from PriorityQueue.py file
from PriorityQueue import PriorityQueue
# Import the Luby Sequence Generator methods from LubyGenerator.py file
from LubyGenerator import reset_luby, get_next_luby_number
# In[2]:
class Statistics:
"""
Class used to store the various statistics measuerd while solving
the SAT problem and defines method to print the statistics.
Public Attributes:
None
Public Methods:
print_stats(): Prints the statistics gathered during the solving of the SAT instance
"""
def __init__(self):
'''
Constructor for the Statistics class.
Arguments:
None
Return:
intialized Statistics object
'''
# Input file in which the problem is stored
self._input_file = ""
# Result of the SAT solver (SAT or UNSAT)
self._result = ""
# Path of the output statistics file used to store
# the statistics for the solved problem
self._output_statistics_file = ""
# Path of the output assignment file which stores the satisfying assignment
# if the problem is satisfied, it is empty if the problem is UNSAT
self._output_assignment_file = ""
# Number of variables in the problem
self._num_vars = 0
# Original number of clauses present in the problem
self._num_orig_clauses = 0
# Number of original clauses stored
# (The unary clauses are not stored and are directly used
# to get the assignment)
self._num_clauses = 0
# Number of clauses learned by the solver
# during the conflict analysis
self._num_learned_clauses = 0
# Number of decisions made by the solver
self._num_decisions = 0
# Number of implications made by the
# solver (These are assignments which are
# not decided but are implied from the decisions)
self._num_implications = 0
# Time at which the solver starts solving the problem
self._start_time = 0
# Time at which the solver is done reading the problem
self._read_time = 0
# Time at which the solver has completed solving the problem
self._complete_time = 0
# Time which the solver spend while performing BCP
self._bcp_time = 0
# Time which the solver spend while deciding (in _decide method)
self._decide_time = 0
# Time which the solver spend while analyzing the conflicts
self._analyze_time = 0
# Time which the solver spend while backtracking
self._backtrack_time = 0
# Number of restarts
self._restarts = 0
def print_stats(self):
'''
Method to print the statistics.
Arguments:
None
Return:
None
'''
# Print the stored statistics with appropriate labels of what the stats signify
print("=========================== STATISTICS ===============================")
print("Solving formula from file: ",self._input_file)
print("Vars:{}, Clauses:{} Stored Clauses:{}".format(str(self._num_vars),str(self._num_orig_clauses),str(self._num_clauses)))
print("Input Reading Time: ",self._read_time - self._start_time)
print("-------------------------------")
print("Restarts: ",self._restarts)
print("Learned clauses: ",self._num_learned_clauses)
print("Decisions made: ",self._num_decisions)
print("Implications made: ",self._num_implications)
print("Time taken: ",self._complete_time-self._start_time)
print("----------- Time breakup ----------------------")
print("BCP Time: ",self._bcp_time)
print("Decide Time: ",self._decide_time)
print("Conflict Analyze Time: ",self._analyze_time)
print("Backtrack Time: ",self._backtrack_time)
print("-------------------------------")
print("RESULT: ",self._result)
print("Statistics stored in file: ",self._output_statistics_file)
# Check if the result of the problem is
# SAT and if it is, then show the
# assignement file name
if self._result == "SAT":
print("Satisfying Assignment stored in file: ",self._output_assignment_file)
print("======================================================================")
# In[3]:
class AssignedNode:
"""
Class used to store the information about the variables being assigned.
Public Attributes:
var: variable that is assigned
value: value assigned to the variable (True/False)
level: level (decision level in the tree) at which the variable is assigned
clause: The id of the clause which implies this decision (If this is assigned through implication)
index: The index in the assignment stack at which this node is pushed
Public Methods:
None
"""
def __init__(self,var,value,level,clause):
'''
Constructor for the AssignedNode class.
Arguments:
var: variable that is assigned
value: value assigned to the variable (True/False)
level: level (decision level in the tree) at which the variable is assigned
clause: The id of the clause which implies this decision (If this is assigned through implication)
Return:
initialized AssignedNode object
'''
# Variable that is assigned
self.var = var
# Value assigned to the variable (True/False)
self.value = value
# Level at which the variable is assigned
self.level = level
# The index of the clause which implies the variable var if var is assigned through Implication
# If var is decided, this is set to None
self.clause = clause
# Index at which a node is placed in the assignment stack
# Initially it is -1 when node is created and has to be
# updated when pushed in assignment_stack.
self.index = -1
def __str__(self):
'''
Method to get the string representation of the AssignedNode object.
Parameters:
None
Return:
a string that has the information about this node
'''
return_string = ""
# Add variable info
return_string += "Var: {} ".format(self.var)
# Add value info
return_string += "Val: {} ".format(self.value)
# Add level info
return_string += "Lev: {} ".format(self.level)
# Add clause info
return_string += "Cls: {} ".format(self.clause)
# Add index info
return_string += "Ind: {} ".format(self.index)
# Return the final string
return return_string
# # Input File Format
#
# The input file is in the DIMACS CNF format. The problem in the DIMACS CNF format looks like this:
#
# c A sample .cnf file. <br>
# p cnf 3 2 <br>
# 1 -3 0 <br>
# 2 3 -1 0 <br>
#
# The lines starting with 'c' are the comments. The line starting with 'p' has the number of variables and the number of clauses after "cnf". Like, in the above example, there are 3 variables and 2 clauses. The next lines of the file are the clauses. Each clause ends with 0. In the input format, each variable is defined by a number. So, we have literals like 1, -2, 3 and so on. Let there be V number of variables. Positive literals are like 1,2,3,4...V and we store them like this only. For the negative literals, -1,-2,..-V, we will add V to its absolute value. That is, -1,-2,-3 ... -V will be represented by V+1,V+2..V+V.
#
# For eg. if we have 3 variables 1,2 and 3. Then the clause (1,-2,3) will be stored as (1,5,3) as -2 is replaced by 2+3=5
# <hr style="border:2px solid gray"> </hr>
#
# # 2-Watched Literals to fasten BCP
#
# Studies show that majority of the times, the SAT solver performs the Boolean Constraint Propogation (BCP) which is responsible for making implications after performing propogation once a variable is decided. A naive approach for doing so would be that whenever a variable is set, we traverse each clause and find such clauses where all but 1 literals have been falsed. This would be very expensive as we will have to traverse all the literals every time a decision is made. And as many decisions are made, BCP is indeed an important step and should be done faster.
#
# To fasten the BCP, we observe that if for any clause, we have 2 literals that are not set false, then it can not pariticipate in BCP at this step. This is because as both the literals are not false, then either atleast one is true which means that the clause is already valid and can not be used to conclude anything or both are not set, which means that there are atleast two literals not defined and thus we can not say anything about the clause at this step. So, for all clauses, we keep 2 watch literals all the time with the invariant that each of them is not false if the clause is not satisfied. Now, whenever we set a variable, say 2 as in the above example and we set it to true. This means that the literal -2 (5) has been falsed and so we only go to clauses watched by -2 and search for a new watcher for them. If we get a new watcher for them, then it is fine. Else we are left with only one undecided literal in it (the other watcher) as we get no watcher (not false literal) which means all other literals have been falsed and so the only undecided literal left is the other watcher and that is now implied.
# <hr style="border:2px solid gray"> </hr>
#
# # Decision Heuristics
#
# The time which the solver takes to solve the problem can be reduced drastically by choosing the variable to be assigned and value that it should be assigned intelligently. This is done with the help of decision heuristics used by the SAT Solver. We implement the following three decision heuristics in this solver.
#
# 1. **ORDERED:** As seen above, the variables are represented as numbers 1,2,3 and so on. So, in this ordered decider, we pick the smallest (comparing the numbers representing the variables) unassigned variable and set it to True. To implement it, we traverse the list 1,2,3 .. V in this order and take the first variable which is unassigned and assign it True.
#
# 2. **VSIDS:** In the VSIDS (Variable State Independent Decaying Sum) like strategy, we prefer the literals that recently participated in the conflict resolution and set them first. We create an array of size 2\*number of variables to store the scores of all literals and initialize it with all 0s. While reading the input file, the score of a literal is incremented by 1 whenever it appears in a clause. When ever a conflict occurs and a conflict cluase is created, we increase the score of all literals in that conflict clause by _incr (which is 1 initially) and _incr is increased by 0.75 after each conflict clause creation. This is done to give more weightage to the literals participating in the recent conflicts. While deciding, the unassigned literal with the highest score is chosen and is satisfied (if it is -v, v is set to False and if it is v, v is set to True)
#
# 3. **MINISAT:** This is the heuristic as used by the MINISAT solver. It is on the lines of VSIDS with some modifications. Here, we maintain the score array for all variables (rather than the literals) and intialize the scores to 0. We also maintain a phase array which stores the last truth value assigned to each variable (whenever a variable is assigned, this phase array is updated). While reading the input file, the score of a variable is incremented by 1 whenever a literal corresponding to it appears in a clause. When ever a conflict occurs and a conflict cluase is created, we increase the score of all variables whose literals appear in that conflict clause by _incr (which is 1 initially) and _incr is divided by _decay (0.85) after each conflict clause creation. This is done to give more weightage to the variables participating in the recent conflicts. While deciding, the unassigned variable with the highest score is fixed. It is assigned the value it was assigned previously as stored in the phase array. This is called **phase-saving**. Phase-saving is beneficial as in a sense we are restarting the search for the solution of the problem by assigning the variable with the same value again. Conflicts that occured after this variable's assignment earlier lead to learning of conflict clauses which will now help in avoiding these conflict situations.
#
# To implement the VSIDS and MINISAT heuristic, we implemented a PriorityQueue (in the file [PriorityQueue.py](PriorityQueue.py)) which has efficient methods to add a variable in the queue, increase the score of a variable in the queue, removing the variable from the queue (when assigned) and getting the variable with maximum score from the queue. All these are written efficiently taking O(log(n)) time. The working of the methods can be seen from the PriorityQueue.py which has been fully documented as well.
# <hr style="border:2px solid gray"> </hr>
#
#
# # Restart Heuristics
#
# The restart heuristics mainly take the number of conflicts as the measure of the solver being stuck and restarts the solver when a certain number of conflicts have been reached. This limit is increased after each restart
# Restart techniques used by the state-of-the-art solvers were studied and the following two of them were tried.
#
# 1. **GEOMETRIC:** In this approach, we have a conflict limit and whenever the number of conflicts go beyond the limit, we restart the solver. Before restarting, the number of conflicts is set to 0 as we consider conflicts only after restart and the conflict limit is now increased by multiplying it with a constant (limit multiplier). This increasing of the conflict limit ensures the correctness as the solver sees if the problem can be solved in the fixed number of conflicts and if it is not solved, the limit is relaxed. This technique is named so because the conflict limits follow a **Geometric Series**. In our implementation, we initialize the conflict limit with 512 and keep the limit mulitplier as 2.
#
# 2. **LUBY:** In this approach, the number of conflicts between 2 restarts are determined by the Luby Sequence. The Luby Sequence was given by Luby et. al. in 1993 and has been proven as an optimized startegy for randomized search algorithms. It has also proven to be very successful in SAT algorithms. The Luby sequence is given as:
# ![Images/luby.png](Images/luby.png)
#
# The Luby sequence thus looks like (1,1,2,1,1,2,4,1,1,2,1,1,2,4,8 ..). We have a base (512 in our implementation) and we multiply the base with the next luby number to get the conflict limit. So, the conflict limits in our implementation would vary like (512, 512, 1024, 512, 512, 1024, 2048 ...). Whenever the number of conflicts go beyond the limit, we restart the solver. Before restarting, the number of conflicts is set to 0 as we consider conflicts only after restart and the conflict limit is set to the next number in this list. This strategy tends towards more frequent restarts and completeness is guaranteed by the increasing upper limit on the number of conflicts. The Luby Sequence Generator has been implemented in the file [LubyGenerator.py](LubyGenerator.py) which is fully documented.
#
# <hr style="border:2px solid gray"> </hr>
#
#
# The code below initializes the main SAT class and defines the various methods that the SAT class uses while solving the SAT problem.
# In[4]:
class SAT:
"""
Class to store the data structures that are maintained while solving the SAT problem.
It also stores the statistics of the solved problem and the methods that are used to solve the
SAT problem.
Public Attributes:
stats: The statistics object that has the statistics of the solved SAT problem
Public Methods:
solve(filename): Takes as argument the filename which has the problem instance in the DIMACS CNF format
and solves it
"""
def __init__(self,to_log,decider,restarter=None):
'''
Constructor for the SAT class
Parameters:
to_log: a boolean (True/False) which indicates whether the solver should log the progress while
solving the problem
decider: the decision heuristic used while deciding the next variable to be assigned and the value
to be assigned to it in the _decide method
restarter: the restart strategy to be used by the SAT solver (None set by default, so if nothing is
passed, restarting will not be used)
Return:
initialized SAT object
'''
# Number of clauses stored
self._num_clauses = 0
# Number of variables
self._num_vars = 0
# Decision level (level at which the solver is in backtracking tree)
self._level = 0
# List of clauses where each clause is stored as a list of
# literals as explained above.
self._clauses = []
# Dictionary mapping a literal to the list of clauses the literal watches
self._clauses_watched_by_l = {}
# Dictionary mapping a clause to the list of literals that watch the clause
self._literals_watching_c = {}
# Dictionary mapping the variables to their assignment nodes
# which contains the information about the value of the variable,
# the clause which implied the variable (if it is implied) and
# the level at which the variable is set
self._variable_to_assignment_nodes = {}
# A stack(list) that stores the assignment nodes in order
# of their assignment
self._assignment_stack = []
# Boolean variable that stores whether the solver should
# log progress information while solving the problem
self._is_log = to_log
# The decision heuristic to be used while solving
# the SAT problem.
# The decider must be ORDERED, VSIDS or MINISAT (discussed
# above) (Raise error if it is not one of them)
if decider not in ["ORDERED","VSIDS","MINISAT"]:
raise ValueError('The decider must be one from the list ["ORDERED","VSIDS","MINISAT"]')
self._decider = decider
if restarter == None:
# If no restart strategy passed,
# set _restarter to None
self._restarter = None
else:
if restarter not in ["GEOMETRIC","LUBY"]:
# Check that the passed strategy should be
# one of the GEOMETRIC or LUBY(as discussed above)
# Raise error if it is not one of them
raise ValueError('The restarter must be one from the list ["GEOMETRIC","LUBY"]')
if restarter == "GEOMETRIC":
# If the GEOMETRIC restart strategy is used,
# then initialize the conflict limit with 512
self._conflict_limit = 512
# This is the limit multiplier by which the
# conflict limit will be multiplied after
# each restart
self._limit_mult = 2
# This stores the number of conflicts
# before restart and is set to 0
# at each restart
self._conflicts_before_restart = 0
if restarter == "LUBY":
# If the LUBY restart strategy is used
# Reset the luby sequencer to initialize it
reset_luby()
# We set base (b) as 512 here
self._luby_base = 512
# Intialize the conflict limit with
# base * the first luby number fetched using the
# get_next_luby_number()
self._conflict_limit = self._luby_base * get_next_luby_number()
# This stores the number of conflicts
# before restart and is set to 0
# at each restart
self._conflicts_before_restart = 0
# Set _restarter to the passed restart
# strategy
self._restarter = restarter
# Statistics object used to store the statistics
# of the problem being solved
self.stats = Statistics()
# In[5]:
def is_negative_literal(self,literal):
'''
Method that takes in number representation of a literal and
returns a boolean indicating if it represents a negative literal.
Parameters:
literal: The number representation of the literal
Return:
a Boolean which is True if the passed literal is False,
else it is False
'''
# As discussed above, we add _num_vars to the absolute
# value to get the representation for the negative literals.
# So, we check if its value is greater than _num_vars to see
# if it is negative.
return literal > self._num_vars
# Add the method to the SAT class
SAT._is_negative_literal = is_negative_literal
# In[6]:
def get_var_from_literal(self, literal):
'''
Method that takes in a literal and gives the variable corresponding to it.
Parameters:
literal: the literal whose corresponding variable is needed
Return:
the variable corrsponding to the passed literal
'''
# If the literal is negative, then _num_vars was added to
# the variable to get the literal, so variable can
# obtained by subtracting _num_vars from the literal
if self._is_negative_literal(literal):
return literal - self._num_vars
# If the literal is positive, it is same as the variable
# and so is returned as it is
return literal
# Add the method to the SAT class
SAT._get_var_from_literal = get_var_from_literal
# In[7]:
def add_clause(self,clause):
'''
Method that takes in a clause, processes it and adds in to the
clause database for the problem.
Parameters:
clause: the clause (list of literals) to be added
Return:
None
'''
# Remove the 0 at the end of clause as in the DIMACS CNF format
clause = clause[:-1]
# OrderedDict's fromkeys method makes an
# dictionary from the elements of the list
# clause and we again make a list from it
# to remove dupicates.
# (We could use set here but it does not maintain the order
# and adds randomness)
clause = list(OrderedDict.fromkeys(clause))
# If it is a unary clause, then that unary literal
# has to be set True and so we treat it as a special
# case
if len(clause)==1:
# Get the literal
lit = clause[0]
# Value to be assigned to the variable
# Set it to true initially
value_to_set = True
if lit[0]=='-':
# If the literal is negative,
# then the value of the variable should be
# set False, to satisfy the literal
value_to_set = False
var = int(lit[1:])
else:
# If the literal is positive, value_to_set remains True
var = int(lit)
if var not in self._variable_to_assignment_nodes:
# If the variable has not been assigned yet
# Increment the number of implications as it is an implication
self.stats._num_implications += 1
# Create an AssignmentNode with var, value_to_set, level 0
# and clause None as we are not storing this clause
node = AssignedNode(var,value_to_set,0,None)
# Set the node with var in the dictionary and push it in the
# assignment stack
self._variable_to_assignment_nodes[var] = node
self._assignment_stack.append(node)
# Set the index of the node to the position in stack at which it
# is pushed
node.index = len(self._assignment_stack)-1
# Log if _is_log is true
if self._is_log:
print("Implied(unary): ",node)
else:
# If the variable is assigned, get its node
node = self._variable_to_assignment_nodes[var]
# If the set value does not match with the value_to_set,
# we have an contradiction and this has happened because of
# two conflicting unary clauses in the problem. So, we decide
# that the problem is UNSAT.
if node.value != value_to_set:
# Set the result in stats to UNSAT
self.stats._result = "UNSAT"
# Return 0 to indicate that the problem has been
# solved. Proven UNSAT
return 0
# Everything normal
return 1
# This is the list of number representation of the literals
clause_with_literals = []
for lit in clause:
if lit[0]=='-':
# If literal is negative, then add _num_vars to
# it to get the literal and push it to the list
var = int(lit[1:]) # lit[1:] removes '-' at start
clause_with_literals.append(var+self._num_vars)
# If VSIDS decider is used, then increase the
# score of the literal appearing in the clause
if self._decider == "VSIDS":
self._lit_scores[var+self._num_vars] += 1
# If MINISAT decider is used, then increase the
# score of the variable corresonding to the
# literal appearing in the clause
if self._decider == "MINISAT":
self._var_scores[var] += 1
else:
# If literal is positive, it is same as its variable
var = int(lit)
clause_with_literals.append(var)
# If VSIDS decider is used, then increase the
# score of the literal appearing in the clause
if self._decider == "VSIDS":
self._lit_scores[var] += 1
# If MINISAT decider is used, then increase the
# score of the variable corresonding to the
# literal appearing in the clause
if self._decider == "MINISAT":
self._var_scores[var] += 1
# Set clause id to the number of clauses
clause_id = self._num_clauses
# Append the new clause to the clause list
# and increase the clause counter
self._clauses.append(clause_with_literals)
self._num_clauses += 1
# Make the first 2 literals as watch literals for this clause
# (Maintains the invariant as both are not set and so are not false)
watch_literal1 = clause_with_literals[0]
watch_literal2 = clause_with_literals[1]
# Set the watch literals for the clause to the list containing the 2 watchers
self._literals_watching_c[clause_id] = [watch_literal1,watch_literal2]
# (In python3, setdefault takes in a key and a value and if in the dictionary, that key has not
# been assigned any value, the passed value is set)
# Add this clause_id to the watched clauses list of both the watchers
self._clauses_watched_by_l.setdefault(watch_literal1,[]).append(clause_id)
self._clauses_watched_by_l.setdefault(watch_literal2,[]).append(clause_id)
# Everything normal
return 1
# Add the method to the SAT class
SAT._add_clause = add_clause
# In[8]:
def read_dimacs_cnf_file(self,cnf_filename):
'''
Method that takes in a filename of a file that has a SAT instance
in the DIMACS CNF format and reads it to extract the clauses.
Parameters:
cnf_filename: The filename where the input (in DIMACS CNF format) is stored
Return:
None
'''
cnf_file = open(cnf_filename,"r")
# For all lines in the file
for line in cnf_file.readlines():
# Remove trailing characters at the end of the line using rstrip
line = line.rstrip()
# Split the line with space as delimiter
line = line.split()
# First word of the line
first_word = line[0]
if first_word == "c":
# If it is a comment, ignore it
continue
elif first_word == "p":
# If it is the "p" line
# Get the number of variables
self._num_vars = int(line[2])
# If VSIDS decider is used, then create the
# _lit_scores array of size 2*_num_vars (for
# all literals) and initialize the score of
# all literals by 0
if self._decider == "VSIDS":
self._lit_scores = [0 for i in range(0,2*self._num_vars+1)]
# If MINISAT decider is used, then create the
# _var_scores array to store scores of all the
# variables and initialize it to all zeroes.
# Also, a _phase array is created which stores
# the last assigned value of the variable
# (O for false, 1 for true) (default initialized to 0)
if self._decider == "MINISAT":
self._var_scores = [0 for i in range(0,self._num_vars+1)]
self._phase = [0 for i in range(0,self._num_vars+1)]
# Store the original number of clauses (as given
# by the last word of this line) in the stats object
self.stats._num_orig_clauses = int(line[3])
else:
# If it is a clause, then call the _add_clause method
ret = self._add_clause(line)
# If 0 is returned, then stop reading
# as the problem is proved UNSAT
if ret == 0:
break
# If the VSIDS decider is used
if self._decider == "VSIDS":
# Create a priority queue (max priority queue)
# using the initialized scores
self._priority_queue = PriorityQueue(self._lit_scores)
# _incr is the quantity by which the scores of
# a literal will be increased when it is
# found in a conflict clause
self._incr = 1
# Some variables may be already assigned because
# of being in the unary clauses, so remove both
# the literals corresponding to the variable
for node in self._assignment_stack:
self._priority_queue.remove(node.var)
self._priority_queue.remove(node.var+self._num_vars)
# If MINISAT decider is used
if self._decider == "MINISAT":
# Create a priority queue (max priority queue)
# using the initialized scores
self._priority_queue = PriorityQueue(self._var_scores)
# _incr is the quantity by which the scores of
# a variable will be increased when it is
# found in a conflict clause
self._incr = 1
# It is the value by which the previous
# scores will decay after each conflict
self._decay = 0.85
# Some variables may be already assigned because
# of being in the unary clauses, so remove them
# from the priority queue
for node in self._assignment_stack:
self._priority_queue.remove(node.var)
# Close the input file
cnf_file.close()
# Add the method to the SAT class
SAT._read_dimacs_cnf_file = read_dimacs_cnf_file
# In[9]:
def decide(self):
'''
Method that chooses an uassigned variable and a boolean value for
it and assigns the variable with that value
Parameters:
None
Returns:
-1 if there are no variables to set, else it returns the variable
which is set
'''
# In these if else statements, we see what decider the solver is using
# and then find the var and value_to_set
if self._decider == "ORDERED":
# If ORDERED decider is used, we start from 1 and get the smallest
# unassigned variable and set it to True
var = -1
for x in range(1,self._num_vars+1):
if x not in self._variable_to_assignment_nodes:
var = x
break
value_to_set = True
elif self._decider == "VSIDS":
# If VSIDS decider is used, we get the literal with the highest
# score from the priority queue
literal = self._priority_queue.get_top()
if literal == -1:
# If it is -1, it means the queue is empty
# which means all variables are assigned
# and so we set var to -1
var = -1
else:
# Get the variable associated to the literal
var = self._get_var_from_literal(literal)
# Store if the literal is negative
is_neg_literal = self._is_negative_literal(literal)
# We need to satisfy the literal so if it is
# negative, set the variable to False (which is
# not True) and vice versa
value_to_set = not is_neg_literal
# Remove the lit complementary to
# the above literal as we have fixed the
# variable and so lit is no longer unassigned
if is_neg_literal:
self._priority_queue.remove(var)
else:
self._priority_queue.remove(var+self._num_vars)
elif self._decider == "MINISAT":
# If MINISAT decider is used, we get the variable with the
# highest score from the priority queue
var = self._priority_queue.get_top()
# We use its last assigned value (as stored in the
# _phase array) to set it
if var != -1:
if self._phase[var] == 0:
value_to_set = False
else:
value_to_set = True
# If var is still -1, it means all the variables
# are already assigned and so we return -1
if var == -1:
return -1
# Increase the level by 1 as a decision is made
self._level += 1
# Create a new assignment node with var, value_to_set, level = _level
# and clause None as this node is made through decide and not implication.
# Add this node to the variable to node dictionary, append it to the stack
# and set the index of the new node to the position at which it is pushed
new_node = AssignedNode(var,value_to_set,self._level,None)
self._variable_to_assignment_nodes[var] = new_node
self._assignment_stack.append(new_node)
new_node.index = len(self._assignment_stack)-1
# Increase the number of decisions made in the stats object.
self.stats._num_decisions += 1
# Log if _is_log is true
if self._is_log:
print("Choosen decision: ",end="")
print(new_node)
# return the var which is set
return var
# Add the method to the SAT class
SAT._decide = decide
# In[10]:
def boolean_constraint_propogation(self,is_first_time):
'''
Main method that makes all the implications.
There are two main cases. When it is run for the first time (if is_first_time is True), we can have many
decisions already made due to the implications by unary clauses and so we have to traverse through all and
make further implications. So, we start at the 0th index in the assignment list. If is_first_time is False,
it means that we only have to take the last made decision into account and make the implications and so we
start from the last node in the assignment stack.
The implied decisions are pushed into the stack until no more implications can be made and "NO_CONFLICT"
is returned, or a conflict is detected and in that case "CONFLICT" is returned. If the number of conflicts
reach a certain limit set by the restart heuristic, then the method returns "RESTART" and restarts the
solver.
Parameters:
is_first_time: Boolean which is set to True when this method is run initially and False for all
other invocations
Return:
"CONFLICT" or "NO_CONFLICT" depending on whether a conflict arised while making the
implications or not. Returns "RESTART" depending on the number of conflicts encountered
and the restart strategy used by the solver (if any)
'''
# Point to the last decision
last_assignment_pointer = len(self._assignment_stack)-1
# If first time, then point to 0
if is_first_time:
last_assignment_pointer = 0
# Traverse through all the assigned nodes in the stack
# and make implications
while last_assignment_pointer < len(self._assignment_stack):
# Get the assigned node
last_assigned_node = self._assignment_stack[last_assignment_pointer]
# If the variable's value was set to True, then negative literal corresponding to
# the variable is falsed, else if it set False, the positive literal
# is falsed
if last_assigned_node.value == True:
literal_that_is_falsed = last_assigned_node.var + self._num_vars
else:
literal_that_is_falsed = last_assigned_node.var
# Now we change the watch literals for all clauses watched by literal_that_is_falsed
itr = 0
# Get the list of clauses watched by the falsed literal
clauses_watched_by_falsed_literal = self._clauses_watched_by_l.setdefault(literal_that_is_falsed,[]).copy()
# We iterate the list of clauses in reverse order as the conflict clauses
# are to the end and we feel using them first is beneficial
clauses_watched_by_falsed_literal.reverse()
# Traverse through them and find a new watch literal and if we are unable to
# find a new watch literal, we have an implication (because of the other watch literal)
# If other watch literal is set to a value opposite of what is implied, we have a
# conflict
while itr < len(clauses_watched_by_falsed_literal):
# Get the clause and its watch list
clause_id = clauses_watched_by_falsed_literal[itr]
watch_list_of_clause = self._literals_watching_c[clause_id]
# Get the other watch literal for this clause
# (other than the falsed one)
other_watch_literal = watch_list_of_clause[0]
if other_watch_literal == literal_that_is_falsed:
other_watch_literal = watch_list_of_clause[1]
# Get the variable corresponding to the watch literal
# and see if the other watch literal is negative
other_watch_var = self._get_var_from_literal(other_watch_literal)
is_negative_other = self._is_negative_literal(other_watch_literal)
# If other watch literal is set and is set so as to be true,
# move to the next clause as this clause is already satisfied
if other_watch_var in self._variable_to_assignment_nodes:
value_assgned = self._variable_to_assignment_nodes[other_watch_var].value
if (is_negative_other and value_assgned == False) or (not is_negative_other and value_assgned == True):
itr += 1
continue
# We need to find a new literal to watch
new_literal_to_watch = -1
clause = self._clauses[clause_id]
# Traverse through all literals
for lit in clause:
if lit not in watch_list_of_clause:
# Consider literals that are not watchers now
var_of_lit = self._get_var_from_literal(lit)
if var_of_lit not in self._variable_to_assignment_nodes:
# If the literal is not set, it can be used as a watcher as it is
# not False
new_literal_to_watch = lit
break
else:
# If the literal's variable is set in such a way that the literal is
# true, we use it as new watcher as anyways the clause is satisfied
node = self._variable_to_assignment_nodes[var_of_lit]
is_negative = self._is_negative_literal(lit)
if (is_negative and node.value == False) or (not is_negative and node.value == True):
new_literal_to_watch = lit
break
if new_literal_to_watch != -1:
# If new_literal_to_watch is not -1, then it means that we have a new literal to watch the
# clause
# Remove the falsed literal and add the new literal to watcher list
# of the clause
self._literals_watching_c[clause_id].remove(literal_that_is_falsed)
self._literals_watching_c[clause_id].append(new_literal_to_watch)
# Remove clause from the watched clauses list of the falsed literal
# and add it to the watched clauses list of the new literal
self._clauses_watched_by_l.setdefault(literal_that_is_falsed,[]).remove(clause_id)
self._clauses_watched_by_l.setdefault(new_literal_to_watch,[]).append(clause_id)
else:
if other_watch_var not in self._variable_to_assignment_nodes:
# We get no other watcher that means all the literals other than
# the other_watch_literal are false and the other_watch_literal
# has to be made true for this clause to be true. This is possible
# in this case as variable corresponding to the other_watch_literal
# is not set.
# Get the value to set the variable as not of if the other watch literal
# is negative. If it is negative (is_negative_other is True), then its variable
# should be set False (not True) and vice_versa
value_to_set = not is_negative_other
# Create the AssignedNode with the variable, value_to_set, level and
# clause_id to refer the clause which is responsible to imply this.
# Then, store it in the variable to assignment dictionary with key
# other_watch_var.
assign_var_node = AssignedNode(other_watch_var,value_to_set,self._level,clause_id)