├── .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 |
--------------------------------------------------------------------------------