class LeafSet(object): __slots__ = ('peers', 'capacity') __passthru = {'get', 'clear', 'pop', 'popitem', 'peekitem', 'key'} __iters = {'keys', 'values', 'items'} def __init__(self, my_key, iterable=(), capacity=8): try: iterable = iterable.items() # view object except AttributeError: pass tuple_itemgetter = Peer.distance(my_key, itemgetter(0)) key_itemgetter = Peer.distance(my_key) self.capacity = capacity self.peers = SortedDict(key_itemgetter) if iterable: l = sorted(iterable, key=tuple_itemgetter) self.peers.update(islice(l, capacity)) def clear(self): self.peers.clear() def prune(self): extra = len(self) - self.capacity for i in range(extra): self.peers.popitem(last=True) def update(self, iterable): try: iterable = iterable.items() # view object except AttributeError: pass iterable = iter(iterable) items = tuple(islice(iterable, 500)) while items: self.peers.update(items) items = tuple(islice(iterable, 500)) def setdefault(self, *args, **kwargs): self.peers.setdefault(*args, **kwargs) self.prune() def __setitem__(self, *args, **kwargs): self.peers.__setitem__(*args, **kwargs) self.prune() def __getitem__(self, *args, **kwargs): return self.peers.__getitem__(*args, **kwargs) def __delitem__(self, *args, **kwargs): return self.peers.__delitem__(*args, **kwargs) def __iter__(self, *args, **kwargs): return self.peers.__iter__(*args, **kwargs) def __reversed__(self, *args, **kwargs): return self.peers.__reversed__(*args, **kwargs) def __contains__(self, *args, **kwargs): return self.peers.__contains__(*args, **kwargs) def __len__(self, *args, **kwargs): return self.peers.__len__(*args, **kwargs) def __getattr__(self, key): if key in self.__class__.__passthru: return getattr(self.peers, key) elif key in self.__class__.__iters: return getattr(self.peers, 'iter' + key) else: return super().__getattr__(key) def __repr__(self): return '<%s keys=%r capacity=%d/%d>' % ( self.__class__.__name__, list(self), len(self), self.capacity)
class Solver: def __init__(self,nvars_,nclauses_): super(Solver,self).__init__() # VARIABLES TO STORE THE CNF RELATED ATTRIBUTES self.nvars = nvars_ # ==> variable to store the total number of variables in SAT formula self.nclauses = nclauses_ # ==> variable to store the total number of clauses in the SAT formula (including unary clauses) self.nlits = 2*nvars_ # ==> variable to store the total number of literals in the SAT formula self.cnf = [] # ==> list to hold the clause objects (excluding unary clauses) self.unit_c = [] # ==> list to hold the unary clauses (only literal not the clause object as in self.cnf) #VARIABLES USED FOR BCP, CONFLICT ANALYSIS AND BACKPROPOGATION self.watches = {indx : [] for indx in range(-nvars_,nvars_+1)} # ==> watch list for each literal :: A dictionary with literal as the key and # value as the list of indexes of clauses in which that literal is one of the # watch literal. As literal can be negative hence dictionary is used instead # of a list of list. self.lit_state = [0 for i in range(self.nvars+1)] # ==> a list to store the state (0,1,-1) of the literal self.lit_prev_state = [0 for i in range(self.nvars+1)] # ==> a list to store the previous state of the literal (PHASE-SAVING) self.BCP_stack = [] # ==> stack which contains the literals which should be propogated through BCP # Stack is used here instead of a queue as , we are traversing in DFS manner. self.dl_var = [-1 for i in range(self.nvars+1)] # ==> A list to store the decision level at which a variable is assigned. self.dl = 0 # ==> Variable to store the current level of the solver self.trail = {} # ==> A dictionary to store the sequence of the assignments of the literal. The key of # the dictionary is the decision level and value is a list of the literals in the order # in which they were assigned in that particular decision level. # The format can be shown as {<decision_level>: [l1,l2,l3,l4,...] } , l1,l2,.. are literals # assigned in the decision level <decision_level> in the order l1,l2,l3,........ self.antecedent = [-1 for i in range(self.nvars+1)] # ==> a list to store the antecedent clause of the literal.(see the definition of # antecendent clause from the writeup) # VARIABLES USED FOR DECIDE :: self.var_score = [0 for i in range(self.nvars+1)] # ==> list to store the scores of the variables based on VSIDS heuristics. Initial # scores of each variable is the number of clauses in which it appears and value # is increased when that variable is involved in the learned clause (Clause Learning). self.score_map = SortedDict() # ==> A dictionary which maps the scores to the variable having that score. Key is score and # and value is a list of variables having that score. This is used to decrease the complexity # of searching the highest score unassigned variable in decision. self.max_score = 0 # ==> A variable to store the current maximum score. Using this we can directly access the # the variable list using self.score_map. This reduces the access time in decision. self.score_map_r_it = self.score_map.__reversed__() # ==> A reverse iterator to iterate from highest to lowest score in self.score_map # VARIABLES USED FOR RESTART :: self.LUBY_FACTOR = 100 # ==> Factor to be multiplied to the LUBY series which is used in Restart self.inner = 0 # ==> inner list pointer of the luby series self.outer = 0 # ==> outer list pointer of the luby series self.u = 1 # ==> heler variables u,v to generate the next number of luby series self.v = 1 # ==> stores the current number in the luby series self.luby_list = [100] # ==> list to store the luby series multiplied by LUBY_FACTOR self.nconflicts = 0 # ==> variable to store the number of conflicts seen so far from the last restart. # If the number of conflicts reaches the value of inner series then restart is # is done and this values is set to 0. self.restarts = 0 #number of restarts self.learned_clause =0 #number of learned clause self.decisions = 0 # number of decisions self.implications = 0 # number of implications # SOLVER FUNCTIONS ::::::::::: # DESCRIPTION :: This function returns the state of the literal. A literal is unassigned (that is L_UNASSIGNED) if it's variable state is 0 and # it is satisfied (that is L_SAT) if it is negative literal and it's variable state is -1 (then literal has state 1) and # if it postive literal and it's variable state is 1 (then literal has state 1). In all other cases the literal is unsatisfied. # AEGUEMENTS :: lit ==> A literal whose state is required. def get_lit_state(self,lit): var = abs(lit) # calculate the respective variable of the literal state = self.lit_state[var] # get the state of that variable if state == 0: # if state == 0, literal is unassigned return LitState.L_UNASSIGNED elif ((lit<0 and state == -1) or (lit>0 and state == 1)) == 1: # if any one condition given before the function defintion for satisiability # of literal holds return satisfied return LitState.L_SAT else: return LitState.L_UNSAT # otherwise return unsatisfied # DESCRIPTION :: This function sets the left and right watches of the clause as the literals present at postion l and r in the clause, inserts # the clause in the watch list of the literals and add it to the cnf. # ARGUMENTS :: C ==> Clause which is to be added # l ==> Index of left watch literal # r ==> Index of right watch literal def add_clause(self,C,l,r): C.lw = C.c[l] # set the left and write literals. C.rw = C.c[r] self.cnf.append(C) # add the clause in the cnf formula # CLAUSE IS 0 INDEXED, LITERAL IS 1 INDEXED size = len(self.cnf)-1 # add the clause in the watch list of each literal. self.watches[C.lw].append(size) self.watches[C.rw].append(size) # DESCRIPTION :: This function add the unary clause. This function does not add unary clause into the cnf formula. Instead assign the value to # the literal as true as this literal can only be true for the formula to be satisfiable and hence unit propogate it's negation. # ARGUMENTS :: C ==> The unary clause object def add_unary_clause(self,C): cl = C.c # get the clause from the Clause object self.assert_unary(cl[0]) # assign the literal its required value,i.e 1 if positive literal and -1 otherwise self.BCP_stack.append(-1*(cl[0])) # add literal's negation in the BCP stack to propogate it through BCP self.unit_c.append(cl[0]) # add the literal into the unit clause list # HEURISTICS USED :: The heuristic used for BCP is based on two watched literal proposed in "Chaff: Engineering an Efficient SAT Solver" paper. # The explanation about heuristic is given in the writeup. # DESCRIPTION :: This function propogates the literals in the BCP stack, using Binary Constraint Propogation (BCP) and also updates the # watch list corresponding to the literal being propogated. As the unit propogated variable is assigned -1 (L_UNSAT state), # a new literal (which is not false) must replace this literal in the clauses in which this literal is a watch literal. # If a new literal is found, watch is updated. If not then if other watch literal of the clause if true then the clause is satisfied # and we continue to the next clause in the watch list of the literal else if other watch literal is unassigned then the clause becomes unit # as all other literals in that clause are false and then other watch literal is unit propogated and If the other watch literal is # false then sovler is arrived at a conflict and conflict analysis and backtracking is done. def BCP(self): while len(self.BCP_stack)!=0: lit = self.BCP_stack.pop() updated_watch_list = [] # ==> updated list of clauses in which literal is watch after unit propogation of literal conflict_cl_indx = None # ==> a variable to store the conflicting clause (in which conflict is produced) index. for i,cl_indx in enumerate(self.watches[lit]): # ==> iterate over the clauses in which literal is a watch literal cl_lw = self.cnf[cl_indx].lw # ==> get the left and right watch literal cl_rw = self.cnf[cl_indx].rw is_left_watch = 1 other_watch = cl_rw if lit == cl_rw: # ==> if literal is right watch literal then other watch literal is left watch literal is_left_watch = 0 # hence set the other watch literal other_watch = cl_lw new_watch = self.cnf[cl_indx].get_next_watch(other_watch,self) # ==> get the next not false literal in the clause. # ==> If new watch is not found then watch is not changed hence push the clause in if new_watch == None: # the updated watch list of literal updated_watch_list.append(cl_indx) if new_watch != None: # ==> if new watch literal is found, update the watch accordingly. if is_left_watch == 1: self.cnf[cl_indx].lw = new_watch else: self.cnf[cl_indx].rw = new_watch self.watches[new_watch].append(cl_indx) # ==> push the clause in the watch list of the new watch literal else: # ==> else if new watch literal is not found if self.get_lit_state(other_watch) == LitState.L_UNSAT: # if the other watch literal state is unsatisfiable then if self.dl==0: # if decision literal is 0 then SAT is unsatifiable as we can backjump to return SolverState.UNSAT, None # flip a variable. self.BCP_stack.clear() # else if decision level != 0, clear the BCP stack as the backjump will reset the # assigned state (to unassigned) of the literals in the BCP stack as BCP stack only # contains the variables at the current decision literal. conflict_cl_indx = cl_indx # set the conflicting clause index for j in range(i+1,len(self.watches[lit])): # add all other clauses into the watch lits of the literal as no change in their watch literals. updated_watch_list.append(self.watches[lit][j]) break # break the loop to perform conflict analysis elif self.get_lit_state(other_watch) == LitState.L_SAT: # ==> else if other watch is satisfied then continue to next clause continue else: # ==> else the clause is unit as the other watch is the only unassigned # literal of the clause self.assert_lit(other_watch) # Assign the literal its value (that is true) self.BCP_stack.append(-1*(other_watch)) # Make the clause as antecedent of the other watch literal. self.antecedent[abs(other_watch)] = cl_indx self.watches[lit].clear() # ==> Update the watch list of the literal. self.watches[lit] = updated_watch_list if conflict_cl_indx != None: # ==> If there exists a conflict return the solver state as the conflict and return SolverState.CONFLICT, conflict_cl_indx; # also return the conflicting clause index return SolverState.UNDEF, None # ==> else solver is in undef state and a decision is taken next. # DESCRIPTION :: This function increases the score of the literal by 1 and updates the scores_map accordingly. # ARGUEMENTS :: var ==> variable whose score is to be increased def increase_score(self,var): old_score = self.var_score[var] # ==> get the old score and calculate the new score score = self.var_score[var]+1 self.var_score[var] = score # ==> set the new score of the variable if self.score_map.get(old_score): # ==> Update the data structure holding the (scores,[literals_list]) key-value self.score_map[old_score].discard(var) # pair according to the changed score. That is delete the previous entry if len(self.score_map[old_score])==0: del self.score_map[old_score] if not self.score_map.get(score): self.score_map[score] = SortedSet() self.score_map[score].add(var) # and add the new entry # HEURISTICS USED :: The confilct analysis is based on "CLAUSE LEARNING" using "IMPLICATION GRAPH" as by Zhang in "Efficient Conflict Driven Learning # in a Boolean Satisfiability Solver" paper. Each literal maintains an antecedent clause which is the clause due to which the unit # propogation of that literal is happened when that clause bacame unit. The detailed explanation is written in writeup. # DESCRIPTION :: This function learns the reason clause by performing resolution of current clause and antecedent clause. The implementation first # selects the current clause as the conflicting clause and adds the literals not in the conflicting level (at which conflict occurs) to the learned clause and mark # the literals of conflicting level in the current clause. Then, the literal which is last assigned literal is resoluted with its antecedent # clause. This resoluted clause is then made as the current clause and same procedure is continued till we reaches to a literal # which is the only marked literal. This literal becomes the 1-UIP in the implication graph and its negation is added in the learned clause. # The second highest decision level of the literals in the learned clause is made as the backjump level. This is done because doing so # makes the learned clause as unit clause (reason in writup) and we get a new literal to propogate just after the backtracking. # ARGUEMENTS :: conflict_c ==> conflicting clause in which conflict is occured during BCP. def conflict_analysis(self,conflict_cl): current_cl = conflict_cl # ==> make the conflicting clause as the current clause confl_level_lit = 0 # ==> variables to hold the marked literals at the conflicting level, reason_cl = [] # to hold learned clause bcktrack_l = 0 # to store backtrack level second_hl_lit = 0 # to store one of the other watch literal of the learned clause trail_indx = len(self.trail[self.dl])-1 # reverse iterator to the trail visited = [0 for i in range(self.nvars+1)] # visited array to mark the literals lit = None # literal and variable to hold the 1-UIP var = None while True: for lit in current_cl: # ==> iterate over the current clause var = abs(lit) var_level = self.dl_var[var] if var_level== self.dl and visited[var]==0: # ==> if literal's decision level is conflicting level mark it and increas the counter visited[var]=1 confl_level_lit +=1 elif var_level!= self.dl and (lit not in reason_cl): # else add it in the reason clause if it is not already present in it. reason_cl.append(lit) self.increase_score(abs(lit)) # ncrease the VSIDS score of literal as it is involved in conflict. if var_level > bcktrack_l: # ==> set the backrack level to the highest level in the learned clause which will bcktrack_l = var_level # beacome second highest when 1-UIP (belongs to conflicting level) is added second_hl_lit = len(reason_cl)-1 while trail_indx >= 0: # ==> get the last assigned literal in the trail which is marked by iterating in the lit = self.trail[self.dl][trail_indx] # trail of the conflicting level in the reverse order. var = abs(lit) trail_indx -= 1 if visited[var]==1: # if mark is true, we have found the last assigned literal break visited[var] = 2 confl_level_lit -= 1 # ==> decrease the counter as we will resolute one of the marked literal if confl_level_lit == 0: # ==> if none other literal is marked else one then break the loop as we have found break # 1-UIP ant = self.antecedent[var] # ==> else calculate the antecendent of the last assigned literal current_cl = self.cnf[ant].c.copy() # get the antecedent clause (use copy so that original clause do not get changed) current_cl.remove(lit) # remove the literal from the clause which is resolution of the last assigned literal UIP1 = -1*(lit) reason_cl.append(UIP1) # ==> add the negation of the 1-UIP in the reason/learned clause self.increase_score(abs(UIP1)) # ==> Increase the VSIDS score of the UIP as it is involved in conflict # ==> add the uip in the BCP stack for the unit propogation as the clause will become # unit after backpropogation if len(reason_cl)==1: self.BCP_stack.append(lit) # If it is unit ,add it into the unit clauses self.unit_c.append(UIP1) else: self.BCP_stack.append(lit) C = Clause() C.c = reason_cl self.add_clause(C,second_hl_lit,len(reason_cl)-1) # ==> else add it into the cnf with other clauses self.learned_clause +=1 self.nconflicts+=1 # ==> increase the number of conflict which is be used for restart. return UIP1, bcktrack_l # ==> return the UIP and backtrack level for backtracking # HEURISTICS USED :: The heuristics used for deciding the literal is to select the literal having maximum score. The scoring method is based on the # "VSIDS" proposed in "Chaff: Engineering an Efficient SAT Solver" paper. The defualt score of each variable is the number in # which that variable appears and increased by 1 if that var occurs in conflict. # The heuristics used for selecting the value of the decided variable is based on "PHASE SAVING", which involves setting the # value of the variable as the last value it was assigned. # DESCRIPTION :: This function selects and assign value to a unassigned variable based on the above heuristic. If a vaiable is not found that is # all the varibles are assigned then it returns Solver State as SAT, otherwise it propogates the selected literal. def decide(self): best_var = None # ==> variable to store the decided variable while True: if self.score_map.get(self.max_score): # ==> iterate from the current max score of the unassigned variables for var in self.score_map[self.max_score]: # in the reverse order. # print("HAIIII ") if self.lit_state[var] == 0: # ==> If a variable is found, break the loop best_var = var break if best_var==None: # If variable is not found in the list of variables having max score, then hasNext = next(self.score_map_r_it,None) if hasNext == None: # if the max score is the minimum score then break the loop as no variable break # be decided. else: self.max_score = hasNext # else go the score which is just less than the current maxscore else: break if best_var == None: # ==> If no variable is decided, return SAT. return SolverState.SAT self.decisions +=1 best_lit = None # ==> select the literal based on the last assigned state of variable if self.lit_prev_state[best_var] == 1: # ==> If previous saved state was 1 then it must be positive literal corresponding best_lit = best_var # to the variable, hence make decided literal as positive literal else: best_lit = -1*best_var # ==> If it was unassigned or assigned -1, select negative literal. # Selecting negative when uassigned is a good choice as in SAT assignments of # variables in a cnf are mostly false. self.dl = self.dl+1 # ==> increase the decision level of the solver self.assert_lit(best_lit) # ==> assign the value to the literal and add it into the trail self.BCP_stack.append(-1*best_lit) # ==> do the BCP return SolverState.UNDEF # DESRIPTION :: This function assign the state to the variable based on the literal. As passed literal is assigned true, hence state of the # variable is made 1 if the literal is positive and -1 if it is negative. Literal is added to the trail at its decision level # ARGUMENT :: lit ==> literal to be assigned and added to the trail def assert_lit(self,lit): self.implications +=1 var = abs(lit) if lit<0: # ==> if lit is negative self.lit_state[var] = self.lit_prev_state[var] = -1 # update previous state and current_state as -1 else: self.lit_state[var] = self.lit_prev_state[var] = 1 # else update to +1 if not self.trail.get(self.dl): # ==> if the current literal is the decision literal then a new level is added in the trail self.trail[self.dl] = [] self.trail[self.dl].append(lit) # ==> literal is added into the trail at its decision level. self.dl_var[var] = self.dl # ==> set the decision level of the literal as the current level # DESRIPTION :: This function assign the state to the unary variable based on the unary literal. The logic of assignment is same as above function. # This function donot add the literal to the trail as it is unary and the value of the literal is fixed (that is true) and saves the # time for reassigning after the backtrack or restart. def assert_unary(self,lit): self.implications +=1 var = abs(lit) if lit<0: self.lit_state[var] = self.lit_prev_state[var] = -1 else: self.lit_state[var] = self.lit_prev_state[var] = 1 self.dl_var[var] = 0 # DESCRIPTION :: This function performs "non chronological backtracking technique" and backtracks to a level calculated in the conflict anaysis. This function # resets the literals decision level, states of their variables and the antecedent clauses of the literals for the level > backtrack level. # This function also resets the max_score as new variables will become unassigned. After resetting the variables and the max_score, it assigns the # value to the UIP calculated in the conflict analysis. This is done after resetting, otherwise backtracking will reset its value. # ARGUMENTNS :: bcktrack_l ==> level till which backtracking it to be done # UIP ==> UIP literal calculated from the conflct analysis. def backtrack(self,bcktrack_l,UIP): level = bcktrack_l+1 # ==> set the starting resetting level and start resetting the variables from this level for dl in range(level,self.dl+1): # ==> iterate over the decision level to reset if self.trail.get(dl): for lit in self.trail[dl]: # ==> iterate over the trail of that decision level self.lit_state[abs(lit)] = 0 # ==> reset the state, decision level and antecedents self.dl_var[abs(lit)] = -1 self.antecedent[abs(lit)] = -1 self.max_score = max(self.max_score,self.var_score[abs(lit)]) # ==> update the max score self.score_map_r_it = self.score_map.__reversed__() curr_sc = next(self.score_map_r_it,None) while curr_sc != None and curr_sc!= self.max_score: # ==> reset the reverse iterator to the new position where curr_sc = next(self.score_map_r_it,None) # max score lies if curr_sc == None: # ==> reset if max_score is not longer valid that is not present self.score_map_r_it = self.score_map.__reversed__() # (A check, condition may never becomes true) max_score = next(self.score_map_r_it) while level <= self.dl: # ==> erase the trail till the backtrack level if self.trail.get(level): del self.trail[level] level+=1 var = abs(UIP) self.dl = bcktrack_l # ==> set the current level as the backtrack level if bcktrack_l == 0: # ==> if backtrack level ==0, the uip must be unary, as self.assert_unary(UIP) # level 0 is the lowest level and hence only single literal will # be present in the learned clause suring conflict analysis else: # ==> else assign the state to the UIP variable and add it into the trail self.assert_lit(UIP) self.antecedent[var] = len(self.cnf)-1 # ==> set it's antecendent clause # DESCRIPTION :: This function return the next number in the luby series. THIS IMPLEMENTED FUNCTION IS TAKEN FROM THE SAT4J SAT SOLVER # http://www.sat4j.org/maven233/org.sat4j.core/xref/org/sat4j/minisat/restarts/LubyRestarts.html def get_next_luby(self): if (self.u & -1*self.u) == self.v: # Luby series seqeunce is 1,1,2,1,1,2,4,1,1,2,1,1,2,4,8,1,1,2....... self.u = self.u+1 # where v gives the luby sequence and can be calculated from the method self.v = 1 # given in this function else: self.v = self.v<<1 return self.v # HEURISTICS :: The restart is based on the iterative two nested luby series. The two series inner and outer are maintained. # Whenever the number of conflicts become equal to the value of inner series, restart is done and inner series is incremented. # Whenever inner series becomes equal to outer series, inner series is reset to starting value and outer series is incremented. # More details are given in the writeup. # DESCRIPTION :: This funtion performs the restart and reset all the variables except the unit ones. It does not delete the learned clause. It is # to backtracking till level 0. def restart(self): self.restarts +=1 for i in range(1,self.nvars+1): # ==> reset the variables if i not in self.unit_c or -i not in self.unit_c: self.lit_state[i] = 0 self.dl_var[i]= -1 self.antecedent[i] = -1 self.trail.clear() # ==> clear the trail and bcp stack self.BCP_stack.clear() self.nconflicts = 0 # ==> reset the number of conflicts self.score_map_r_it = self.score_map.__reversed__() # ==> reset the max score self.max_score = next(self.score_map_r_it) self.dl=0 # ==> set the decision level to 0 if self.inner == self.outer: # ==> if the inner luby series has reached to outer luby series, next_luby = self.get_next_luby()*self.LUBY_FACTOR # ==> get the next luby number self.luby_list.append(next_luby) # ==> append in the luby list self.outer += 1 # ==> increment the outer luby series self.inner = 0 # ==> reset the inner luby series to initial value else: self.inner += 1 # ==> else increment the inner luby series