A propositional formula $\Phi$ in Conjunctive Normal Form (CNF) is a conjunction of clauses $\omega_j$, with $j \in \{1,...,m\}$. Each clause being a disjunction of literals and each literal being either a positive ($x_i$) or a negative ($\lnot{x_i}$) propositional variable, with $i \in \{1,...,n\}$. By denoting with $[\lnot]$ the possible presence of $\lnot$, we can formally define $\Phi$ as:
$$\bigwedge_{j = 1,...,m}\bigg(\bigvee_{i \in \omega_j} [\lnot] x_i\bigg)$$The Boolean Satisfiability Problem (SAT) consists in determining whether there exists a truth assignment in $\{0, 1\}$ (or equivalently in $\{True,False\}$) for the variables in $\Phi$.
In [1]:
from logic import *
The Davis-Putnam-Logemann-Loveland (DPLL) algorithm is a complete (will answer SAT if a solution exists) and sound (it will not answer SAT for an unsatisfiable formula) procedue that combines backtracking search and deduction to decide satisfiability of propositional logic formula in CNF. At each search step a variable and a propositional value are selected for branching purposes. With each branching step, two values can be assigned to a variable, either 0 or 1. Branching corresponds to assigning the chosen value to the chosen variable. Afterwards, the logical consequences of each branching step are evaluated. Each time an unsatisfied clause (ie a conflict) is identified, backtracking is executed. Backtracking corresponds to undoing branching steps until an unflipped branch is reached. When both values have been assigned to the selected variable at a branching step, backtracking will undo this branching step. If for the first branching step both values have been considered, and backtracking undoes this first branching step, then the CNF formula can be declared unsatisfiable. This kind of backtracking is called chronological backtracking.
Essentially, DPLL
is a backtracking depth-first search through partial truth assignments which uses a splitting rule to replaces the original problem with two smaller subproblems, whereas the original Davis-Putnam procedure uses a variable elimination rule which replaces the original problem with one larger subproblem. Over the years, many heuristics have been proposed in choosing the splitting variable (which variable should be assigned a truth value next).
Search algorithms that are based on a predetermined order of search are called static algorithms, whereas the ones that select them at the runtime are called dynamic. The first SAT search algorithm, the Davis-Putnam procedure is a static algorithm. Static search algorithms are usually very slow in practice and for this reason perform worse than dynamic search algorithms. However, dynamic search algorithms are much harder to design, since they require a heuristic for predetermining the order of search. The fundamental element of a heuristic is a branching strategy for selecting the next branching literal. This must not require a lot of time to compute and yet it must provide a powerful insight into the problem instance.
Two basic heuristics are applied to this algorithm with the potential of cutting the search space in half. These are the pure literal rule and the unit clause rule.
In [2]:
%psource dpll_satisfiable
In [3]:
%psource dpll
Each of these branching heuristics was applied only after the pure literal and the unit clause heuristic failed in selecting a splitting variable.
MOMs heuristics are simple, efficient and easy to implement. The goal of these heuristics is to prefer the literal having Maximum number of Occurences in the Minimum length clauses. Intuitively, the literals belonging to the minimum length clauses are the most constrained literals in the formula. Branching on them will maximize the effect of BCP and the likelihood of hitting a dead end early in the search tree (for unsatisfiable problems). Conversely, in the case of satisfiable formulas, branching on a highly constrained variable early in the tree will also increase the likelihood of a correct assignment of the remained open literals. The MOMs heuristics main disadvatage is that their effectiveness highly depends on the problem instance. It is easy to see that the ideal setting for these heuristics is considering the unsatisfied binary clauses.
In [4]:
%psource min_clauses
In [5]:
%psource moms
Over the years, many types of MOMs heuristics have been proposed.
MOMSf choose the variable $x$ with a maximize the function:
$$[f(x) + f(\lnot{x})] * 2^k + f(x) * f(\lnot{x})$$where $f(x)$ is the number of occurrences of $x$ in the smallest unknown clauses, k is a parameter.
In [6]:
%psource momsf
Freeman’s POSIT [1] version counts both the number of positive $x$ and negative $\lnot{x}$ occurrences of a given variable $x$.
In [7]:
%psource posit
Zabih and McAllester’s [2] version of the heuristic counts the negative occurrences $\lnot{x}$ of each given variable $x$.
In [8]:
%psource zm
Literal count heuristics count the number of unresolved clauses in which a given variable $x$ appears as a positive literal, $C_P$ , and as negative literal, $C_N$. These two numbers an either be onsidered individually or ombined.
Dynamic Largest Individual Sum heuristic considers the values $C_P$ and $C_N$ separately: select the variable with the largest individual value and assign to it value true if $C_P \geq C_N$, value false otherwise.
In [9]:
%psource dlis
Dynamic Largest Combined Sum considers the values $C_P$ and $C_N$ combined: select the variable with the largest sum $C_P + C_N$ and assign to it value true if $C_P \geq C_N$, value false otherwise.
In [10]:
%psource dlcs
Two branching heuristics were proposed by Jeroslow and Wang in [3].
The one-sided Jeroslow and Wang’s heuristic compute:
$$J(l) = \sum_{l \in \omega \land \omega \in \phi} 2^{-|\omega|}$$and selects the assignment that satisfies the literal with the largest value $J(l)$.
In [11]:
%psource jw
The two-sided Jeroslow and Wang’s heuristic identifies the variable $x$ with the largest sum $J(x) + J(\lnot{x})$, and assigns to $x$ value true, if $J(x) \geq J(\lnot{x})$, and value false otherwise.
In [12]:
%psource jw2
The Conflict-Driven Clause Learning (CDCL) solver is an evolution of the DPLL algorithm that involves a number of additional key techniques:
The first difference between a DPLL solver and a CDCL solver is the introduction of the non-chronological backtracking or backjumping when a conflict is identified. This requires an iterative implementation of the algorithm because only if the backtrack stack is managed explicitly it is possible to backtrack more than one level.
In [4]:
%psource cdcl_satisfiable
The second important difference between a DPLL solver and a CDCL solver is that the information about a conflict is reused by learning: if a conflicting clause is found, the solver derive a new clause from the conflict and add it to the clauses database.
Whenever a conflict is identified due to unit propagation, a conflict analysis procedure is invoked. As a result, one or more new clauses are learnt, and a backtracking decision level is computed. The conflict analysis procedure analyzes the structure of unit propagation and decides which literals to include in the learnt clause. The decision levels associated with assigned variables define a partial order of the variables. Starting from a given unsatisfied clause (represented in the implication graph with vertex $\kappa$), the conflict analysis procedure visits variables implied at the most recent decision level (ie the current largest decision level), identifies the antecedents of visited variables, and keeps from the antecedents the literals assigned at decision levels less than the most recent decision level. The clause learning procedure used in the CDCL can be defined by a sequence of selective resolution operations, that at each step yields a new temporary clause. This process is repeated until the most recent decision variable is visited.
The structure of implied assignments induced by unit propagation is a key aspect of the clause learning procedure. Moreover, the idea of exploiting the structure induced by unit propagation was further exploited with Unit Implication Points (UIPs). A UIP is a dominator in the implication graph and represents an alternative decision assignment at the current decision level that results in the same conflict. The main motivation for identifying UIPs is to reduce the size of learnt clauses. Clause learning could potentially stop at any UIP, being quite straightforward to conclude that the set of literals of a clause learnt at the first UIP has clear advantages. Considering the largest decision level of the literals of the clause learnt at each UIP, the clause learnt at the first UIP is guaranteed to contain the smallest one. This guarantees the highest backtrack jump in the search tree.
In [5]:
%psource conflict_analysis
In [6]:
%psource pl_binary_resolution
In [7]:
%psource backjump
Implementation issues for SAT solvers include the design of suitable data structures for storing clauses. The implemented data structures dictate the way BCP are implemented and have a significant impact on the run time performance of the SAT solver. Recent state-of-the-art SAT solvers are characterized by using very efficient data structures, intended to reduce the CPU time required per each node in the search tree. Conversely, traditional SAT data structures are accurate, meaning that is possible to know exactly the value of each literal in the clause. Examples of the most recent SAT data structures, which are not accurate and therefore are called lazy, include the watched literals used in Chaff .
The more recent Chaff SAT solver [4] proposed a new data structure, the 2 Watched Literals (2WL), in which two references are associated with each clause. There is no order relation between the two references, allowing the references to move in any direction. The lack of order between the two references has the key advantage that no literal references need to be updated when backtracking takes place. In contrast, unit or unsatisfied clauses are identified only after traversing all the clauses’ literals; a clear drawback. The two watched literal pointers are undifferentiated as there is no order relation. Again, each time one literal pointed by one of these pointers is assigned, the pointer has to move inwards. These pointers may move in both directions. This causes the whole clause to be traversed when the clause becomes unit. In addition, no references have to be kept to the just assigned literals, since pointers do not move when backtracking.
In [8]:
%psource unit_propagation
In [9]:
%psource TwoWLClauseDatabase
The early branching heuristics made use of all the information available from the data structures, namely the number of satisfied, unsatisfied and unassigned literals. These heuristics are updated during the search and also take into account the clauses that are learnt.
More recently, a different kind of variable selection heuristic, referred to as Variable State Independent Decaying Sum (VSIDS), has been proposed by Chaff authors in [4]. One of the reasons for proposing this new heuristic was the introduction of lazy data structures, where the knowledge of the dynamic size of a clause is not accurate. Hence, the heuristics described above cannot be used. VSIDS selects the literal that appears most frequently over all the clauses, which means that one counter is required for each one of the literals. Initially, all counters are set to zero. During the search, the metrics only have to be updated when a new recorded clause is created. More than to develop an accurate heuristic, the motivation has been to design a fast (but dynamically adapting) heuristic. In fact, one of the key properties of this strategy is the very low overhead, due to being independent of the variable state.
In [10]:
%psource assign_decision_literal
Solving NP-complete problems, such as SAT, naturally leads to heavy-tailed run times. To deal with this, SAT solvers frequently restart their search to avoid the runs that take disproportionately longer. What restarting here means is that the solver unsets all variables and starts the search using different variable assignment order.
While at first glance it might seem that restarts should be rare and become rarer as the solving has been going on for longer, so that the SAT solver can actually finish solving the problem, the trend has been towards more aggressive (frequent) restarts.
The reason why frequent restarts help solve problems faster is that while the solver does forget all current variable assignments, it does keep some information, specifically it keeps learnt clauses, effectively sampling the search space, and it keeps the last assigned truth value of each variable, assigning them the same value the next time they are picked to be assigned.
In this strategy, the number of conflicts between 2 restarts is based on the Luby sequence. The Luby restart sequence is interesting in that it was proven to be optimal restart strategy for randomized search algorithms where the runs do not share information. While this is not true for SAT solving, as shown in [5] and [6], Luby restarts have been quite successful anyway.
The exact description of Luby restarts is that the $ith$ restart happens after $u \cdot Luby(i)$ conflicts, where $u$ is a constant and $Luby(i)$ is defined as:
$$Luby(i) = \begin{cases} 2^{k-1} & i = 2^k - 1 \\ Luby(i - 2^{k-1} + 1) & 2^{k-1} \leq i < 2^k - 1 \end{cases} $$A less exact but more intuitive description of the Luby sequence is that all numbers in it are powers of two, and after a number is seen for the second time, the next number is twice as big. The following are the first 16 numbers in the sequence:
$$ (1,1,2,1,1,2,4,1,1,2,1,1,2,4,8,1,...) $$From the above, we can see that this restart strategy tends towards frequent restarts, but some runs are kept running for much longer, and there is no upper limit on the longest possible time between two restarts.
In [9]:
%psource luby
Glucose restarts were popularized by the Glucose solver, and it is an extremely aggressive, dynamic restart strategy. The idea behind it and described in [7] is that instead of waiting for a fixed amount of conflicts, we restart when the last couple of learnt clauses are, on average, bad.
A bit more precisely, if there were at least $X$ conflicts (and thus $X$ learnt clauses) since the last restart, and the average Literal Block Distance (LBD) (a criterion to evaluate the quality of learnt clauses as shown in [8] of the last $X$ learnt clauses was at least $K$ times higher than the average LBD of all learnt clauses, it is time for another restart. Parameters $X$ and $K$ can be tweaked to achieve different restart frequency, and they are usually kept quite small.
In [10]:
%psource glucose
In [22]:
from csp import *
In [23]:
australia_csp = MapColoringCSP(list('RGB'), """SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: """)
In [24]:
%time _, checks = AC3b(australia_csp, arc_heuristic=dom_j_up)
f'AC3b with DOM J UP needs {checks} consistency-checks'
Out[24]:
In [25]:
%time backtracking_search(australia_csp, select_unassigned_variable=mrv, inference=forward_checking)
Out[25]:
In [2]:
australia_sat = MapColoringSAT(list('RGB'), """SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: """)
In [3]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=no_branching_heuristic)
In [28]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=moms)
In [29]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=momsf)
In [30]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=posit)
In [31]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=zm)
In [32]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=dlis)
In [33]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=dlcs)
In [34]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=jw)
In [35]:
%time model = dpll_satisfiable(australia_sat, branching_heuristic=jw2)
In [36]:
%time model = cdcl_satisfiable(australia_sat)
In [37]:
{var for var, val in model.items() if val}
Out[37]:
In [38]:
france_csp = MapColoringCSP(list('RGBY'),
"""AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA
AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO
CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR:
MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO:
PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA:
AU BO FC PA LR""")
In [39]:
%time _, checks = AC3b(france_csp, arc_heuristic=dom_j_up)
f'AC3b with DOM J UP needs {checks} consistency-checks'
Out[39]:
In [40]:
%time backtracking_search(france_csp, select_unassigned_variable=mrv, inference=forward_checking)
Out[40]:
In [41]:
france_sat = MapColoringSAT(list('RGBY'),
"""AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA
AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO
CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR:
MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO:
PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA:
AU BO FC PA LR""")
In [42]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=no_branching_heuristic)
In [43]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=moms)
In [44]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=momsf)
In [45]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=posit)
In [46]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=zm)
In [47]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=dlis)
In [48]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=dlcs)
In [49]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=jw)
In [50]:
%time model = dpll_satisfiable(france_sat, branching_heuristic=jw2)
In [51]:
%time model = cdcl_satisfiable(france_sat)
In [52]:
{var for var, val in model.items() if val}
Out[52]:
In [53]:
usa_csp = MapColoringCSP(list('RGBY'),
"""WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;
UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX AZ;
ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;
TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;
LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL;
MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;
PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ;
NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;
HI: ; AK: """)
In [54]:
%time _, checks = AC3b(usa_csp, arc_heuristic=dom_j_up)
f'AC3b with DOM J UP needs {checks} consistency-checks'
Out[54]:
In [55]:
%time backtracking_search(usa_csp, select_unassigned_variable=mrv, inference=forward_checking)
Out[55]:
In [56]:
usa_sat = MapColoringSAT(list('RGBY'),
"""WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;
UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX AZ;
ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;
TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;
LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL;
MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;
PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ;
NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;
HI: ; AK: """)
In [57]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=no_branching_heuristic)
In [58]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=moms)
In [59]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=momsf)
In [60]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=posit)
In [61]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=zm)
In [62]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=dlis)
In [63]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=dlcs)
In [64]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=jw)
In [65]:
%time model = dpll_satisfiable(usa_sat, branching_heuristic=jw2)
In [66]:
%time model = cdcl_satisfiable(usa_sat)
In [67]:
{var for var, val in model.items() if val}
Out[67]:
In [76]:
zebra_csp = Zebra()
In [77]:
zebra_csp.display(zebra_csp.infer_assignment())
In [78]:
%time _, checks = AC3b(zebra_csp, arc_heuristic=dom_j_up)
f'AC3b with DOM J UP needs {checks} consistency-checks'
Out[78]:
In [71]:
zebra_csp.display(zebra_csp.infer_assignment())
In [72]:
%time backtracking_search(zebra_csp, select_unassigned_variable=mrv, inference=forward_checking)
Out[72]:
In [5]:
zebra_sat = associate('&', map(to_cnf, map(expr, filter(lambda line: line[0] not in ('c', 'p'), open('aima-data/zebra.cnf').read().splitlines()))))
In [14]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=no_branching_heuristic)
In [16]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=moms)
In [17]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=momsf)
In [18]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=posit)
In [19]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=zm)
In [19]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=dlis)
In [21]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=dlcs)
In [20]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=jw)
In [75]:
%time model = dpll_satisfiable(zebra_sat, branching_heuristic=jw2)
In [7]:
%time model = cdcl_satisfiable(zebra_sat)
In [8]:
{var for var, val in model.items() if val and var.op.startswith(('Englishman', 'Japanese', 'Norwegian', 'Spaniard', 'Ukrainian'))}
Out[8]:
[[1]](#ref-1) Freeman, Jon William. 1995. Improvements to propositional satisfiability search algorithms.
[[2]](#ref-2) Zabih, Ramin and McAllester, David A. 1988. A Rearrangement Search Strategy for Determining Propositional Satisfiability.
[[3]](#ref-3) Jeroslow, Robert G and Wang, Jinchang. 1990. Solving propositional satisfiability problems.
[[4]](#ref-4) Moskewicz, Matthew W and Madigan, Conor F and Zhao, Ying and Zhang, Lintao and Malik, Sharad. 2001. Chaff: Engineering an efficient SAT solver.
[[5]](#ref-5) Haim, Shai and Heule, Marijn. 2014. Towards ultra rapid restarts.
[[6]](#ref-6) Huang, Jinbo and others. 2007. The Effect of Restarts on the Efficiency of Clause Learning.
[[7]](#ref-7) Audemard, Gilles and Simon, Laurent. 2012. Refining restarts strategies for SAT and UNSAT.
[[8]](#ref-8) Audemard, Gilles and Simon, Laurent. 2009. Predicting learnt clauses quality in modern SAT solvers.