diff --git a/README.md b/README.md
index c97db60f1..a793deb30 100644
--- a/README.md
+++ b/README.md
@@ -98,9 +98,9 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and
| 7.10 | TT-Entails | `tt_entails` | [`logic.py`][logic] | Done | Included |
| 7.12 | PL-Resolution | `pl_resolution` | [`logic.py`][logic] | Done | Included |
| 7.14 | Convert to CNF | `to_cnf` | [`logic.py`][logic] | Done | Included |
-| 7.15 | PL-FC-Entails? | `pl_fc_resolution` | [`logic.py`][logic] | Done | |
-| 7.17 | DPLL-Satisfiable? | `dpll_satisfiable` | [`logic.py`][logic] | Done | |
-| 7.18 | WalkSAT | `WalkSAT` | [`logic.py`][logic] | Done | |
+| 7.15 | PL-FC-Entails? | `pl_fc_resolution` | [`logic.py`][logic] | Done | Included |
+| 7.17 | DPLL-Satisfiable? | `dpll_satisfiable` | [`logic.py`][logic] | Done | Included |
+| 7.18 | WalkSAT | `WalkSAT` | [`logic.py`][logic] | Done | Included |
| 7.20 | Hybrid-Wumpus-Agent | `HybridWumpusAgent` | | | |
| 7.22 | SATPlan | `SAT_plan` | [`logic.py`][logic] | Done | |
| 9 | Subst | `subst` | [`logic.py`][logic] | Done | |
diff --git a/logic.ipynb b/logic.ipynb
index 726a8d69d..0cd6cbc1f 100644
--- a/logic.ipynb
+++ b/logic.ipynb
@@ -1489,6 +1489,853 @@
"pl_resolution(wumpus_kb, ~P22), pl_resolution(wumpus_kb, P22)"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Effective Propositional Model Checking\n",
+ "\n",
+ "The previous segments elucidate the algorithmic procedure for model checking. \n",
+ "In this segment, we look at ways of making them computationally efficient.\n",
+ "
\n",
+ "The problem we are trying to solve is conventionally called the _propositional satisfiability problem_, abbreviated as the _SAT_ problem.\n",
+ "In layman terms, if there exists a model that satisfies a given Boolean formula, the formula is called satisfiable.\n",
+ "
\n",
+ "The SAT problem was the first problem to be proven _NP-complete_.\n",
+ "The main characteristics of an NP-complete problem are:\n",
+ "- Given a solution to such a problem, it is easy to verify if the solution solves the problem.\n",
+ "- The time required to actually solve the problem using any known algorithm increases exponentially with respect to the size of the problem.\n",
+ "
\n",
+ "
\n",
+ "Due to these properties, heuristic and approximational methods are often applied to find solutions to these problems.\n",
+ "
\n",
+ "It is extremely important to be able to solve large scale SAT problems efficiently because \n",
+ "many combinatorial problems in computer science can be conveniently reduced to checking the satisfiability of a propositional sentence under some constraints.\n",
+ "
\n",
+ "We will introduce two new algorithms that perform propositional model checking in a computationally effective way.\n",
+ "
\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 1. DPLL (Davis-Putnam-Logeman-Loveland) algorithm\n",
+ "This algorithm is very similar to Backtracking-Search.\n",
+ "It recursively enumerates possible models in a depth-first fashion with the following improvements over algorithms like `tt_entails`:\n",
+ "1. Early termination:\n",
+ "
\n",
+ "In certain cases, the algorithm can detect the truth value of a statement using just a partially completed model.\n",
+ "For example, $(P\\lor Q)\\land(P\\lor R)$ is true if P is true, regardless of other variables.\n",
+ "This reduces the search space significantly.\n",
+ "2. Pure symbol heuristic:\n",
+ "
\n",
+ "A symbol that has the same sign (positive or negative) in all clauses is called a _pure symbol_.\n",
+ "It isn't difficult to see that any satisfiable model will have the pure symbols assigned such that its parent clause becomes _true_.\n",
+ "For example, $(P\\lor\\neg Q)\\land(\\neg Q\\lor\\neg R)\\land(R\\lor P)$ has P and Q as pure symbols\n",
+ "and for the sentence to be true, P _has_ to be true and Q _has_ to be false.\n",
+ "The pure symbol heuristic thus simplifies the problem a bit.\n",
+ "3. Unit clause heuristic:\n",
+ "
\n",
+ "In the context of DPLL, clauses with just one literal and clauses with all but one _false_ literals are called unit clauses.\n",
+ "If a clause is a unit clause, it can only be satisfied by assigning the necessary value to make the last literal true.\n",
+ "We have no other choice.\n",
+ "
\n",
+ "Assigning one unit clause can create another unit clause.\n",
+ "For example, when P is false, $(P\\lor Q)$ becomes a unit clause, causing _true_ to be assigned to Q.\n",
+ "A series of forced assignments derived from previous unit clauses is called _unit propagation_.\n",
+ "In this way, this heuristic simplifies the problem further.\n",
+ "
\n",
+ "The algorithm often employs other tricks to scale up to large problems.\n",
+ "However, these tricks are currently out of the scope of this notebook. Refer to section 7.6 of the book for more details.\n",
+ "
\n",
+ "
\n",
+ "Let's have a look at the algorithm."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "
def dpll(clauses, symbols, model):\n",
+ " """See if the clauses are true in a partial model."""\n",
+ " unknown_clauses = [] # clauses with an unknown truth value\n",
+ " for c in clauses:\n",
+ " val = pl_true(c, model)\n",
+ " if val is False:\n",
+ " return False\n",
+ " if val is not True:\n",
+ " unknown_clauses.append(c)\n",
+ " if not unknown_clauses:\n",
+ " return model\n",
+ " P, value = find_pure_symbol(symbols, unknown_clauses)\n",
+ " if P:\n",
+ " return dpll(clauses, removeall(P, symbols), extend(model, P, value))\n",
+ " P, value = find_unit_clause(clauses, model)\n",
+ " if P:\n",
+ " return dpll(clauses, removeall(P, symbols), extend(model, P, value))\n",
+ " if not symbols:\n",
+ " raise TypeError("Argument should be of the type Expr.")\n",
+ " P, symbols = symbols[0], symbols[1:]\n",
+ " return (dpll(clauses, symbols, extend(model, P, True)) or\n",
+ " dpll(clauses, symbols, extend(model, P, False)))\n",
+ "def dpll_satisfiable(s):\n",
+ " """Check satisfiability of a propositional sentence.\n",
+ " This differs from the book code in two ways: (1) it returns a model\n",
+ " rather than True when it succeeds; this is more useful. (2) The\n",
+ " function find_pure_symbol is passed a list of unknown clauses, rather\n",
+ " than a list of all clauses and the model; this is more efficient."""\n",
+ " clauses = conjuncts(to_cnf(s))\n",
+ " symbols = list(prop_symbols(s))\n",
+ " return dpll(clauses, symbols, {})\n",
+ "def WalkSAT(clauses, p=0.5, max_flips=10000):\n",
+ " """Checks for satisfiability of all clauses by randomly flipping values of variables\n",
+ " """\n",
+ " # Set of all symbols in all clauses\n",
+ " symbols = {sym for clause in clauses for sym in prop_symbols(clause)}\n",
+ " # model is a random assignment of true/false to the symbols in clauses\n",
+ " model = {s: random.choice([True, False]) for s in symbols}\n",
+ " for i in range(max_flips):\n",
+ " satisfied, unsatisfied = [], []\n",
+ " for clause in clauses:\n",
+ " (satisfied if pl_true(clause, model) else unsatisfied).append(clause)\n",
+ " if not unsatisfied: # if model satisfies all the clauses\n",
+ " return model\n",
+ " clause = random.choice(unsatisfied)\n",
+ " if probability(p):\n",
+ " sym = random.choice(list(prop_symbols(clause)))\n",
+ " else:\n",
+ " # Flip the symbol in clause that maximizes number of sat. clauses\n",
+ " def sat_count(sym):\n",
+ " # Return the the number of clauses satisfied after flipping the symbol.\n",
+ " model[sym] = not model[sym]\n",
+ " count = len([clause for clause in clauses if pl_true(clause, model)])\n",
+ " model[sym] = not model[sym]\n",
+ " return count\n",
+ " sym = argmax(prop_symbols(clause), key=sat_count)\n",
+ " model[sym] = not model[sym]\n",
+ " # If no solution is found within the flip limit, we return failure\n",
+ " return None\n",
+ "