├── .gitignore ├── README.md ├── __init__.py ├── carroll.py ├── nodes.py ├── normal_forms.py ├── parsing.py ├── proofs.py ├── symbols.py └── truthtable.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carroll 2 | Carroll is a **C**ommand-line **A**pp for **R**apidly **R**emoving **O**bstacles to **L**earning **L**ogic. 3 | 4 |

Usage

5 | Example: using Carroll to get a truth table for an expression. 6 | ``` 7 | $ python carroll.py table "((A&B) v (~A&~B))" --verbose 8 | A B True 9 | ~A B False 10 | A ~B False 11 | ~A ~B True 12 | 13 | Satisfiable: True 14 | Tautology: False 15 | ``` 16 | Example: using Carroll to check the validity of modus ponens. 17 | ``` 18 | $ python carroll.py proof 19 | > (A>B) 20 | > A 21 | > B 22 | > 23 | Valid 24 | ``` 25 | 26 |

Connectives/operators:

27 | - **AND:** & or ^ 28 | - **OR:** \| or v 29 | - **NOT:** ~ or ! 30 | - **IF:** > 31 | - **IFF:** = 32 | - **XOR:** x 33 | 34 |

Dependencies

35 | Carroll uses Nose and Click. 36 | 37 | 38 |

Commands

39 | 40 | - ```table```: Prints a truth table for an expression. Optionally checks satisfiability and tautology too. 41 | - ```equiv```: Checks two expressions for logical equivalence (i.e. whether they compute the same boolean function) 42 | - ```cnf``` and ```dnf```: Converts an expression to its equivalent in conjunctive or disjunctive normal form. 43 | - ```proof```: Accepts propositions from stdin until an empty proposition is entered. Checks if the last proposition (conclusion) is implied by the previous propositions (premises). 44 | 45 |

Planned features:

46 | 47 | - Check any number of propositions for equivalence, mutual satisfiability, etc 48 | - Simplify expressions 49 | - Use user-defined connectives? 50 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamchalmers/carroll/55f921e7024c52f852baa2781a9e2a1f18912010/__init__.py -------------------------------------------------------------------------------- /carroll.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | import click 5 | 6 | import truthtable 7 | import normal_forms 8 | import proofs 9 | 10 | @click.group() 11 | def cli(): 12 | """Carroll is a command line tool for analysing propositional logic (also known as boolean functions or expressions). 13 | """ 14 | pass 15 | 16 | @cli.command() 17 | @click.argument("expression_1") 18 | @click.argument("expression_2") 19 | def equiv(expression_1, expression_2): 20 | """Checks whether two expressions are logically equivalent.""" 21 | print(truthtable.equivalent(expression_1, expression_2)) 22 | 23 | @cli.command() 24 | @click.argument("expression") 25 | @click.option("--verbose", is_flag=True, default=False, help="Check for satisfiability, validity etc.") 26 | def table(expression, verbose): 27 | """Outputs a truth table for a logical expression.""" 28 | truthtable.print_truth_table(expression, verbose) 29 | 30 | @cli.command() 31 | @click.argument("expression") 32 | def dnf(expression): 33 | """Converts an expression to disjunctive normal form.""" 34 | print(normal_forms.to_dnf(expression)) 35 | 36 | @cli.command() 37 | @click.argument("expression") 38 | def cnf(expression): 39 | """Converts an expression to conjunctive normal form.""" 40 | print(normal_forms.to_cnf(expression)) 41 | 42 | @cli.command() 43 | def proof(): 44 | """Checks a proof for validity.""" 45 | exp = raw_input() 46 | expressions = [] 47 | while exp: 48 | expressions.append(exp) 49 | exp = raw_input() 50 | if proofs.valid_proof(expressions): 51 | print("Valid") 52 | else: 53 | print("Invalid") 54 | 55 | if __name__ == "__main__": 56 | cli() 57 | -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_raises, with_setup 2 | import string 3 | 4 | T = True 5 | F = False 6 | 7 | class LogicError(Exception): 8 | pass 9 | 10 | class Node(object): 11 | """Base class for logic nodes. 12 | 13 | A node forms an expression tree for a sentence of symbolic logic.""" 14 | 15 | def __init__(self, *children): 16 | self.check_valid(children) 17 | self.children = children 18 | 19 | def eval(self, model): 20 | """Evaluates the logic tree rooted at this node against a supplied model. 21 | 22 | Model is an assignment of truth values to atoms (dict of string -> bool).""" 23 | raise NotImplementedError 24 | 25 | def check_valid(self, children): 26 | """Ensures the children nodes are valid. Raises LogicError if they're not.""" 27 | if len(children) == 0: 28 | raise LogicError("%s can't have 0 children!", type(self)) 29 | self.check_valid_specific(children) 30 | 31 | def check_valid_specific(self, children): 32 | """Overridden by children nodes to implement custom child-checking logic. 33 | Should raise LogicError if children are invalid.""" 34 | pass 35 | 36 | def atoms(self): 37 | """Return a set of characters which are atoms in this expression.""" 38 | children_atoms = set() 39 | for child in self.children: 40 | children_atoms = children_atoms | child.atoms() 41 | return children_atoms 42 | 43 | @property 44 | def l(self): 45 | return self.children[0] 46 | 47 | @property 48 | def r(self): 49 | try: 50 | return self.children[1] 51 | except IndexError: 52 | return None 53 | 54 | class AndNode(Node): 55 | def eval(self, model): 56 | return all([n.eval(model) for n in self.children]) 57 | 58 | class OrNode(Node): 59 | def eval(self, model): 60 | return any([n.eval(model) for n in self.children]) 61 | 62 | class NotNode(Node): 63 | def eval(self, model): 64 | if len(self.children) != 1: 65 | raise LogicError("NOT is undefined for multiple children.") 66 | return not self.l.eval(model) 67 | 68 | class IfNode(Node): 69 | def eval(self, model): 70 | if len(self.children) != 2: 71 | raise LogicError("IF is only defined for exactly two children.") 72 | return not self.children[0].eval(model) or self.children[1].eval(model) 73 | 74 | class XorNode(Node): 75 | def eval(self, model): 76 | children_values = [n.eval(model) for n in self.children] 77 | return any(children_values) and not all(children_values) 78 | 79 | class IffNode(Node): 80 | def eval(self, model): 81 | children_values = [n.eval(model) for n in self.children] 82 | return not any(children_values) or all(children_values) 83 | 84 | 85 | class AtomNode(Node): 86 | """These nodes will always form the leaves of a logic tree. 87 | 88 | They are the only node whose children are strings, not other nodes.""" 89 | def eval(self, model): 90 | return model[self.l] 91 | def check_valid_specific(self, children): 92 | if len(children) != 1: 93 | raise LogicError("Can't have multiple atomic propositions in one atom %s" % str(children)) 94 | if children[0] not in string.uppercase: 95 | raise LogicError("Atoms must be capital letters (your atom is %s)" % str(children[0])) 96 | pass 97 | def atoms(self): 98 | return set(self.l) 99 | 100 | 101 | def setup_tf_nodes(): 102 | global a 103 | global b 104 | global model 105 | a = AtomNode("A") 106 | b = AtomNode("B") 107 | model = {"A": T, "B": F} 108 | 109 | def teardown(): 110 | pass 111 | 112 | @with_setup(setup_tf_nodes, teardown) 113 | def test_single_node_eval(): 114 | assert a.eval(model) 115 | assert not b.eval(model) 116 | assert OrNode(a, a).eval(model) 117 | assert OrNode(a, b).eval(model) 118 | assert OrNode(b, a).eval(model) 119 | assert not OrNode(b, b).eval(model) 120 | assert AndNode(a, a).eval(model) 121 | assert not AndNode(a, b).eval(model) 122 | assert not AndNode(b, a).eval(model) 123 | assert not AndNode(b, b).eval(model) 124 | assert not NotNode(a).eval(model) 125 | assert NotNode(b).eval(model) 126 | 127 | @with_setup(setup_tf_nodes, teardown) 128 | def test_if_nodes(): 129 | assert IfNode(a, a).eval(model) 130 | assert not IfNode(a, b).eval(model) 131 | assert IfNode(b, a).eval(model) 132 | assert IfNode(b, b).eval(model) 133 | 134 | @with_setup(setup_tf_nodes, teardown) 135 | def test_xor_nodes(): 136 | assert not XorNode(a, a).eval(model) 137 | assert XorNode(a, b).eval(model) 138 | assert XorNode(b, a).eval(model) 139 | assert not XorNode(b, b).eval(model) 140 | 141 | @with_setup(setup_tf_nodes, teardown) 142 | def test_multiple_xor_nodes(): 143 | c = AtomNode("C") 144 | model = {"A": T, "B": F, "C": F} 145 | assert XorNode(a, b, c).eval(model) 146 | assert XorNode(a, a, c).eval(model) 147 | assert XorNode(b, a, a).eval(model) 148 | assert not XorNode(a, a, a).eval(model) 149 | assert not XorNode(b, c, b).eval(model) 150 | assert not XorNode(b, b, b).eval(model) 151 | 152 | 153 | @with_setup(setup_tf_nodes, teardown) 154 | def test_iff_nodes(): 155 | assert IffNode(a, a).eval(model) 156 | assert not IffNode(a, b).eval(model) 157 | assert not IffNode(b, a).eval(model) 158 | assert IffNode(b, b).eval(model) 159 | 160 | @with_setup(setup_tf_nodes, teardown) 161 | def test_multiple_iff_nodes(): 162 | c = AtomNode("C") 163 | model = {"A": T, "B": F, "C": F} 164 | assert not IffNode(a, b, c).eval(model) 165 | assert not IffNode(a, a, c).eval(model) 166 | assert not IffNode(b, a, a).eval(model) 167 | assert IffNode(a, a, a).eval(model) 168 | assert IffNode(b, c, b).eval(model) 169 | assert IffNode(b, b, b).eval(model) 170 | 171 | @with_setup(setup_tf_nodes, teardown) 172 | def test_compound_node_eval(): 173 | assert NotNode(NotNode(a)).eval(model) 174 | assert NotNode(AndNode(a, b)).eval(model) 175 | assert not NotNode(OrNode(a, b)).eval(model) 176 | assert OrNode(NotNode(AndNode(a,b)), NotNode(OrNode(a, b))).eval(model) 177 | assert NotNode(OrNode(b, b)) 178 | 179 | @with_setup(setup_tf_nodes, teardown) 180 | def test_many_ands(): 181 | assert AndNode(a, a, a).eval(model) 182 | assert AndNode(a, a, a, a).eval(model) 183 | assert not AndNode(a, a, b).eval(model) 184 | assert not AndNode(a, b, a).eval(model) 185 | assert not AndNode(b, a, a).eval(model) 186 | 187 | 188 | @with_setup(setup_tf_nodes, teardown) 189 | def test_many_ors(): 190 | assert OrNode(b, b, a).eval(model) 191 | assert OrNode(b, a, a).eval(model) 192 | assert OrNode(a, b, a).eval(model) 193 | assert OrNode(a, a, b).eval(model) 194 | assert not OrNode(b, b, b).eval(model) 195 | 196 | @with_setup(setup_tf_nodes, teardown) 197 | def test_single_not(): 198 | n = NotNode(a, b) 199 | assert_raises(LogicError, n.eval, model) 200 | 201 | @with_setup(setup_tf_nodes, teardown) 202 | def test_single_atom(): 203 | assert_raises(LogicError, AtomNode, *(a,b)) 204 | 205 | def test_atoms_uppercase(): 206 | assert_raises(LogicError, AtomNode, "a") 207 | 208 | def test_check_valid(): 209 | assert_raises(LogicError, AtomNode) 210 | 211 | @with_setup(setup_tf_nodes, teardown) 212 | def test_simple_find_atoms(): 213 | n = AndNode(a, b) 214 | assert n.atoms() == {"A", "B"} 215 | 216 | @with_setup(setup_tf_nodes, teardown) 217 | def test_find_atoms(): 218 | c = AtomNode("C") 219 | d = AtomNode("D") 220 | n = AndNode(XorNode(a, a), OrNode(b, IffNode(c, d))) 221 | assert n.atoms() == {"A", "B", "C", "D"} -------------------------------------------------------------------------------- /normal_forms.py: -------------------------------------------------------------------------------- 1 | import truthtable 2 | import functools 3 | from nose.tools import assert_equals, assert_items_equal 4 | 5 | def to_dnf(expression): 6 | """Converts a proposition string into a DNF string.""" 7 | table = truthtable.truth_table(expression) 8 | output = "(" 9 | for row in table: 10 | if row.value: 11 | output += "(%s) v " % and_clause(row.model) 12 | output = output[:-3] + ")" 13 | return output 14 | 15 | def to_cnf(expression): 16 | table = truthtable.truth_table(expression) 17 | output = "(" 18 | for row in table: 19 | if not row.value: 20 | output += "(%s) & " % or_clause(row.model) 21 | output = output[:-3] + ")" 22 | return output 23 | 24 | def model_to_clause(model, truth, symbol): 25 | l = [] 26 | for atom, value in model.items(): 27 | if value == truth: 28 | l.append((atom, atom)) 29 | else: 30 | l.append((atom, "~"+atom)) 31 | l.sort() 32 | symbol = " %s " % symbol 33 | return symbol.join([elem[1] for elem in l]) 34 | 35 | and_clause = functools.partial(model_to_clause, truth=True, symbol="&") 36 | or_clause = functools.partial(model_to_clause, truth=False, symbol="v") 37 | 38 | def test_basic_dnf(): 39 | expression = "(A & (B | C))" 40 | expected = "((A & B & C) v (A & B & ~C) v (A & ~B & C))" 41 | actual = to_dnf(expression) 42 | assert_equals(expected, actual) 43 | 44 | def test_longer_dnf(): 45 | expression_cnf = "((~A v ~B v ~C) & (~A v B v ~C) & (~A v B v C) & (A v ~B v ~C) & (A v ~B v C))" 46 | actual = to_dnf(expression_cnf) 47 | expected_dnf = "((A & B & ~C) v (~A & ~B & C) v (~A & ~B & ~C))" 48 | assert_equals(expected_dnf, actual) 49 | 50 | def test_basic_cnf(): 51 | expression = "(~A & (B v C))" 52 | expected = "((~A v ~B v ~C) & (~A v ~B v C) & (~A v B v ~C) & (~A v B v C) & (A v B v C))" 53 | actual = to_cnf(expression) 54 | assert_equals(expected, actual) 55 | 56 | def test_longer_cnf(): 57 | expression_dnf = "((A & B & ~C) v (~A & ~B & C) v (~A & ~B & ~C))" 58 | actual = to_cnf(expression_dnf) 59 | expected_cnf = "((~A v ~B v ~C) & (~A v B v ~C) & (~A v B v C) & (A v ~B v ~C) & (A v ~B v C))" 60 | 61 | # We strip out the opening/closing brackets, and compare the clauses 62 | # so the strangely-ordered CNF conversion doesn't ruin our test. 63 | assert_items_equal(expected_cnf[1:-1].split(" & "), actual[1:-1].split(" & ")) 64 | -------------------------------------------------------------------------------- /parsing.py: -------------------------------------------------------------------------------- 1 | import string 2 | import symbols 3 | from collections import deque 4 | from nose.tools import assert_equals, assert_raises, assert_is_instance 5 | from symbols import meaning_of 6 | from nodes import AtomNode, NotNode, AndNode, OrNode, XorNode, IfNode, IffNode 7 | 8 | def parse(exp): 9 | """Starts parsing a logical expression (supplied as a string). Returns a tree of Nodes.""" 10 | exp = exp.replace(" ", "") 11 | d = deque(exp) 12 | tree = _parse(d) 13 | if d: 14 | raise IOError("Unconsumed tokens %s" % "".join(d)) 15 | else: 16 | return tree 17 | 18 | def _parse(exp): 19 | """Recursive-descent parsing algorithm for logic expressions. Returns a tree of Nodes.""" 20 | 21 | if not exp: 22 | raise IOError("Empty string is not a wff.") 23 | 24 | char = exp.popleft() 25 | 26 | # Atom node case 27 | if meaning_of(char) == AtomNode: 28 | return AtomNode(char) 29 | 30 | # Single-operand node case (i.e. NOT node) 31 | elif meaning_of(char) == NotNode: 32 | return NotNode(_parse(exp)) 33 | 34 | # Multiple-operand node case (e.g. AND, NOT) 35 | elif char == "(": 36 | l = _parse(exp) 37 | _op = exp[0] 38 | more = [] 39 | while exp and exp[0] == _op: 40 | op = exp.popleft() 41 | more.append(_parse(exp)) 42 | if not exp or exp.popleft() != ")": 43 | raise IOError("Missing )") 44 | return meaning_of(op)(l, *more) 45 | else: 46 | raise IOError("%s can't start a wff." % char) 47 | 48 | def test_error_parse(): 49 | assert_raises(IOError, parse, "") 50 | assert_raises(IOError, parse, "(") 51 | assert_raises(IOError, parse, "()") 52 | assert_raises(IOError, parse, "(~)") 53 | assert_raises(IOError, parse, "&") 54 | assert_raises(IOError, parse, "BvC") 55 | assert_raises(IOError, parse, "A&A") 56 | assert_raises(IOError, parse, "(A&BC") 57 | assert_raises(IOError, parse, "(A&B") 58 | 59 | def test_spaces_parse(): 60 | parse("(A & B)") 61 | parse("(A& ~ B)") 62 | 63 | def test_atom_parse(): 64 | assert_is_instance(parse("A"), AtomNode) 65 | assert_is_instance(parse("D"), AtomNode) 66 | assert_is_instance(parse("Q"), AtomNode) 67 | 68 | def test_not_parse(): 69 | n = parse("~~A") 70 | assert_is_instance(n, NotNode) 71 | assert_is_instance(n.l, NotNode) 72 | assert_is_instance(n.l.l, AtomNode) 73 | 74 | def test_simple_and_parse(): 75 | n = parse("(A&B)") 76 | assert_is_instance(n, AndNode) 77 | assert_is_instance(n.l, AtomNode) 78 | assert_is_instance(n.r, AtomNode) 79 | 80 | def test_complex_and_parse(): 81 | n = parse("(~A&B)") 82 | assert_is_instance(n, AndNode) 83 | assert_is_instance(n.l, NotNode) 84 | assert_is_instance(n.l.l, AtomNode) 85 | assert_is_instance(n.r, AtomNode) 86 | 87 | def test_multiple_and_parse(): 88 | n = parse("(A&(B&C))") 89 | assert_is_instance(n, AndNode) 90 | assert_is_instance(n.r, AndNode) 91 | 92 | def test_simple_or_parse(): 93 | n = parse("(AvB)") 94 | assert_is_instance(n, OrNode) 95 | assert_is_instance(n.l, AtomNode) 96 | assert_is_instance(n.r, AtomNode) 97 | 98 | def test_complex_or_parse(): 99 | n = parse("(~A|B)") 100 | assert_is_instance(n, OrNode) 101 | assert_is_instance(n.l, NotNode) 102 | assert_is_instance(n.l.l, AtomNode) 103 | 104 | def test_multiple_or_parse(): 105 | n = parse("(Av(B|C))") 106 | assert_is_instance(n, OrNode) 107 | assert_is_instance(n.r, OrNode) 108 | 109 | def test_complex_parse(): 110 | n = parse("((~A|B)v(B&~C))") 111 | assert_is_instance(n, OrNode) 112 | assert_is_instance(n.l, OrNode) 113 | assert_is_instance(n.l.r, AtomNode) 114 | assert_is_instance(n.l.l, NotNode) 115 | assert_is_instance(n.l.l.l, AtomNode) 116 | assert_is_instance(n.r, AndNode) 117 | assert_is_instance(n.r.l, AtomNode) 118 | assert_is_instance(n.r.r, NotNode) 119 | assert_is_instance(n.r.r.l, AtomNode) 120 | 121 | def test_multiple_operand_parse(): 122 | n = parse("(A&A&A)") 123 | assert_is_instance(n, AndNode) 124 | for child in n.children: 125 | assert_is_instance(child, AtomNode) 126 | 127 | def test_dnf_parse(): 128 | exp = "((A & B & C) v (A & B & ~C) v (A & ~B & C))" 129 | n = parse(exp) 130 | assert_is_instance(n, OrNode) 131 | 132 | def test_multiple_operand_fail(): 133 | assert_raises(IOError, parse, "(A|A&A)") 134 | 135 | def test_parse_if(): 136 | n = parse("(A>B)") 137 | assert_is_instance(n, IfNode) 138 | assert_is_instance(n.l, AtomNode) 139 | assert_is_instance(n.r, AtomNode) 140 | 141 | def test_parse_iff(): 142 | n = parse("(A=B)") 143 | assert_is_instance(n, IffNode) 144 | assert_is_instance(n.l, AtomNode) 145 | assert_is_instance(n.r, AtomNode) 146 | 147 | def test_parse_xor(): 148 | n = parse("(AxB)") 149 | assert_is_instance(n, XorNode) 150 | assert_is_instance(n.l, AtomNode) 151 | assert_is_instance(n.r, AtomNode) -------------------------------------------------------------------------------- /proofs.py: -------------------------------------------------------------------------------- 1 | from parsing import parse 2 | import truthtable 3 | from nodes import AndNode, IfNode, AtomNode 4 | from nose.tools import assert_is_instance 5 | 6 | def serialize_argument_trees(expressions): 7 | """ 8 | Takes in a list of parse tree expressions [A, B, C, ... Z]. 9 | Outputs one parse tree ((A&B&C&...) -> Z). 10 | Last expression is the conclusion, all others are premises. 11 | """ 12 | premises = AndNode(*expressions[:-1]) 13 | argument = IfNode(premises, expressions[-1]) 14 | return argument 15 | 16 | def valid_proof(expressions): 17 | """ 18 | Takes in a list of parse tree expressions [A, B, C, ... Z]. 19 | Outputs one parse tree ((A&B&C&...) -> Z). 20 | Last expression is the conclusion, all others are premises. 21 | """ 22 | trees = [parse(e) for e in expressions] 23 | argument = serialize_argument_trees(trees) 24 | return all([row.value for row in truthtable.from_tree(argument)]) 25 | 26 | def test_serialize_argtree_simple(): 27 | expressions = ["A", "(A>B)", "B"] 28 | trees = [parse(e) for e in expressions] 29 | argument = serialize_argument_trees(trees) 30 | assert_is_instance(argument, IfNode) 31 | assert_is_instance(argument.l, AndNode) 32 | assert_is_instance(argument.r, AtomNode) 33 | assert_is_instance(argument.l.l, AtomNode) 34 | assert_is_instance(argument.l.r, IfNode) 35 | assert_is_instance(argument.l.r.l, AtomNode) 36 | assert_is_instance(argument.l.r.r, AtomNode) 37 | 38 | def test_valid_proof(): 39 | expressions = ["A", "(A>B)", "B"] 40 | assert valid_proof(expressions) 41 | 42 | def test_invalid_proof(): 43 | expressions = ["A", "(AxB)", "B"] 44 | assert not valid_proof(expressions) 45 | -------------------------------------------------------------------------------- /symbols.py: -------------------------------------------------------------------------------- 1 | import string 2 | import nodes 3 | from collections import defaultdict 4 | 5 | NODE_TYPES = [(string.ascii_uppercase, nodes.AtomNode), 6 | ("&^", nodes.AndNode), 7 | ("|v", nodes.OrNode), 8 | ("!~", nodes.NotNode), 9 | ("x", nodes.XorNode), 10 | (">", nodes.IfNode), 11 | ("=", nodes.IffNode), 12 | ] 13 | 14 | # Map every recognized character to its node type. 15 | _symbols = defaultdict(lambda: None) 16 | for symbols, node in NODE_TYPES: 17 | for symbol in symbols: 18 | _symbols[symbol] = node 19 | 20 | 21 | def meaning_of(symbol): 22 | """Returns the type of node this symbol represents.""" 23 | return _symbols[symbol] 24 | 25 | def test_symbol_mapping(): 26 | assert meaning_of("A") == nodes.AtomNode 27 | assert meaning_of("B") == nodes.AtomNode 28 | assert meaning_of("Y") == nodes.AtomNode 29 | assert meaning_of("&") == nodes.AndNode 30 | assert meaning_of("v") == nodes.OrNode 31 | assert meaning_of(")") == None 32 | assert meaning_of("(") == None 33 | assert meaning_of("d") == None 34 | 35 | -------------------------------------------------------------------------------- /truthtable.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import parsing 3 | import symbols 4 | import string 5 | from nose.tools import assert_items_equal, assert_equal 6 | 7 | T = True 8 | F = False 9 | 10 | class Row(): 11 | 12 | def __init__(self, model, value): 13 | self.model = model 14 | self.value = value 15 | 16 | def __str__(self): 17 | return truth_to_str(self.model) + " " + str(self.value) 18 | 19 | def __eq__(self, other): 20 | return isinstance(other, Row) and self.model == other.model and self.value == other.value 21 | 22 | 23 | def print_truth_table(exp, verbose, output=True): 24 | """Outputs the truth table for an expression to stdout.""" 25 | try: 26 | tree = parsing.parse(exp) 27 | table = truth_table(exp) 28 | except IOError as e: 29 | print("Parse error: %s" % e) 30 | return 31 | for row in table: 32 | print(row) 33 | if verbose: 34 | print() 35 | table = truth_table(exp) 36 | print_sat_info(table) 37 | 38 | def truth_table(exp): 39 | """Generates truth table rows from a proposition string.""" 40 | tree = parsing.parse(exp) 41 | atoms = find_atoms(exp) 42 | for truth in gen_truths(atoms): 43 | yield Row(truth, tree.eval(truth)) 44 | 45 | def from_tree(tree): 46 | """Generates a truth table from a parse tree.""" 47 | atoms = list(tree.atoms()) 48 | for truth in gen_truths(atoms): 49 | yield Row(truth, tree.eval(truth)) 50 | 51 | def find_atoms(exp): 52 | """Returns a list of atoms in a proposition string.""" 53 | return list(set([char for char in exp if char in string.ascii_uppercase])) 54 | 55 | def gen_truths(atoms): 56 | """Yields all possible maps of variables to truth values.""" 57 | if len(atoms) == 1: 58 | yield {atoms[0]: T} 59 | yield {atoms[0]: F} 60 | else: 61 | for truth in gen_truths(atoms[1:]): 62 | yield dict([(atoms[0], T)] + truth.items()) 63 | yield dict([(atoms[0], F)] + truth.items()) 64 | 65 | def truth_to_str(truth): 66 | """Produce a nice-formatted string of a truth assignment, e.g. " A ~B C".""" 67 | prefix = {True: " ", False: "~"} 68 | items = truth.items() 69 | items.sort() 70 | return "".join([prefix[value] + var + " " for var, value in items]) 71 | 72 | def all_equal(iterable): 73 | iterable = iter(iterable) 74 | first = iterable.next() 75 | for element in iterable: 76 | if element != first: 77 | return False 78 | return True 79 | 80 | def equivalent(exp1, exp2): 81 | table1, table2 = truth_table(exp1), truth_table(exp2) 82 | return all([row1 == row2 for row1, row2 in zip(table1, table2)]) 83 | 84 | def print_sat_info(table): 85 | satisfiable = False 86 | tautology = True 87 | for row in table: 88 | if row.value: 89 | satisfiable = True 90 | else: 91 | tautology = False 92 | 93 | print("Satisfiable:\t%s" % satisfiable) 94 | print("Tautology:\t%s" % tautology) 95 | 96 | return (satisfiable, tautology) 97 | 98 | # Tests 99 | 100 | def test_gen_truths_base(): 101 | expected = [{"A": True}, {"A": False}] 102 | assert_items_equal(expected, [truth for truth in gen_truths(["A"])]) 103 | 104 | def test_gen_truths_recursive(): 105 | expected = [ 106 | {"A": T, "B": T}, 107 | {"A": T, "B": F}, 108 | {"A": F, "B": T}, 109 | {"A": F, "B": F}, 110 | ] 111 | actual = [truth for truth in gen_truths(["A", "B"])] 112 | assert_items_equal(expected, actual) 113 | 114 | def test_gen_truths_recursive_long(): 115 | expected = [ 116 | {"A": T, "B": T, "C": T}, 117 | {"A": T, "B": T, "C": F}, 118 | {"A": T, "B": F, "C": T}, 119 | {"A": T, "B": F, "C": F}, 120 | {"A": F, "B": T, "C": T}, 121 | {"A": F, "B": T, "C": F}, 122 | {"A": F, "B": F, "C": T}, 123 | {"A": F, "B": F, "C": F}, 124 | ] 125 | actual = [truth for truth in gen_truths(["A", "B", "C"])] 126 | assert_items_equal(expected, actual) 127 | 128 | def test_find_atoms(): 129 | assert_items_equal(find_atoms("ABC"), list("ABC")) 130 | assert_items_equal(find_atoms("A B C"), list("ABC")) 131 | assert_items_equal(find_atoms("Av B&C"), list("ABC")) 132 | assert_items_equal(find_atoms("ABC"), list("ABC")) 133 | 134 | def test_truth_table_and(): 135 | expected_table = [ 136 | Row({"A": T, "B": T}, T), 137 | Row({"A": T, "B": F}, F), 138 | Row({"A": F, "B": T}, F), 139 | Row({"A": F, "B": F}, F), 140 | ] 141 | actual_table = truth_table("(A&B)") 142 | assert_items_equal(expected_table, actual_table) 143 | 144 | def test_truth_table_or(): 145 | expected_table = [ 146 | Row({"A": T, "B": T}, T), 147 | Row({"A": T, "B": F}, T), 148 | Row({"A": F, "B": T}, T), 149 | Row({"A": F, "B": F}, F), 150 | ] 151 | actual_table = truth_table("(AvB)") 152 | assert_items_equal(expected_table, actual_table) 153 | 154 | def test_equiv_simple(): 155 | assert equivalent("A", "A") 156 | assert equivalent("A", "~~A") 157 | 158 | def test_not_equiv_simple(): 159 | assert not equivalent("A", "B") 160 | assert not equivalent("A", "~A") 161 | assert not equivalent("A", "(A&B)") 162 | 163 | def test_equiv_complex(): 164 | assert equivalent("(A&~B)", "(~B&(AvB))") 165 | assert equivalent("(AvB)", "!(!A&!B)") 166 | 167 | def test_not_equiv_complex(): 168 | assert not equivalent("(AvB)", "(!A&!B)") 169 | 170 | def test_all_equal(): 171 | assert all_equal([1,1,1]) 172 | assert all_equal([True]) 173 | assert all_equal([True, True, True]) 174 | assert all_equal([False]) 175 | assert all_equal([False, False, False]) 176 | 177 | def test_not_all_equal(): 178 | assert not all_equal([1,2]) 179 | assert not all_equal([True, False]) 180 | assert not all_equal([True, 2]) 181 | 182 | def test_sat_info_simple(): 183 | assert print_sat_info(truth_table("(Av~A)")) == (T, T) 184 | assert print_sat_info(truth_table("A")) == (T, F) 185 | assert print_sat_info(truth_table("~A")) == (T, F) 186 | assert print_sat_info(truth_table("(A&~A)")) == (F, F) 187 | 188 | def test_sat_info_harder(): 189 | assert print_sat_info(truth_table("(A&B&C)")) == (T, F) 190 | assert print_sat_info(truth_table("(A&B&C&~A)")) == (F, F) 191 | --------------------------------------------------------------------------------