On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote: > > This is a replacement for sympy.core.logic and will be used > extensively by the query module. The old logic module can be > removed once the old assumption system is removed.
The code is good, tests are ok, but please try to add more doctests, so that it's easier for users to understand what each function is doing. I marked the functions where I would add a doctest below: > > Contents > -------- > > sympy.logic.boolalg > - Operators for boolean algebra on symbols {And: &, Or: |, Not: ~} > - some useful methods, mainly used to convert expressions to > conjunctive normal form (CNF) > > sympy.logic.inference > - Rules for inference in propositional logic. Main method here is > satisfiable(expr), which checks satisfiability of a propositional > sentence (for now using dpll algorithm) > > sympy.logic.kb > - Definition of a knowledge base > > sympy.algorithms > - some algorithms used by higher order methods (just DPLL for now), > but in a future we should also have Quine-McCluskey for simplification > of boolean expressions > > Most of the methods in this module are implementations of algorithms described > in the book "AI: A modern approach", http://aima.cs.berkeley.edu/ > --- > sympy/core/basic.py | 26 ++++++- > sympy/logic/__init__.py | 2 + > sympy/logic/algorithms/dpll.py | 49 +++++++++++ > sympy/logic/boolalg.py | 155 > +++++++++++++++++++++++++++++++++++ > sympy/logic/inference.py | 119 +++++++++++++++++++++++++++ > sympy/logic/kb.py | 36 ++++++++ > sympy/logic/tests/test_boolalg.py | 110 +++++++++++++++++++++++++ > sympy/logic/tests/test_inference.py | 46 ++++++++++ > 8 files changed, 541 insertions(+), 2 deletions(-) > create mode 100644 sympy/logic/__init__.py > create mode 100644 sympy/logic/algorithms/__init__.py > create mode 100644 sympy/logic/algorithms/dpll.py > create mode 100755 sympy/logic/boolalg.py > create mode 100755 sympy/logic/inference.py > create mode 100644 sympy/logic/kb.py > create mode 100755 sympy/logic/tests/test_boolalg.py > create mode 100755 sympy/logic/tests/test_inference.py > > diff --git a/sympy/core/basic.py b/sympy/core/basic.py > index b48e633..6ab3101 100644 > --- a/sympy/core/basic.py > +++ b/sympy/core/basic.py > @@ -680,8 +680,30 @@ def __rdiv__(self, other): > __truediv__ = __div__ > __rtruediv__ = __rdiv__ > > - > - > + # ******************* > + # * Logic operators * > + # ******************* > + > + def __and__(self, other): > + """Overloading for & operator""" > + from sympy.logic.boolalg import And > + return And(self, other) > + def __or__(self, other): > + """Overloading for |""" > + from sympy.logic.boolalg import Or > + return Or(self, other) > + def __invert__(self): > + """Overloading for ~""" > + from sympy.logic.boolalg import Not > + return Not(self) > + def __rshift__(self, other): > + """Overloading for >>""" > + from sympy.logic.boolalg import Implies > + return Implies(self, other) > + def __lshift__(self, other): > + """Overloading for <<""" > + from sympy.logic.boolalg import Implies > + return Implies(other, self) > > > def __repr__(self): > diff --git a/sympy/logic/__init__.py b/sympy/logic/__init__.py > new file mode 100644 > index 0000000..5076249 > --- /dev/null > +++ b/sympy/logic/__init__.py > @@ -0,0 +1,2 @@ > +from boolalg import * > +from inference import * > \ No newline at end of file > diff --git a/sympy/logic/algorithms/__init__.py > b/sympy/logic/algorithms/__init__.py > new file mode 100644 > index 0000000..e69de29 > diff --git a/sympy/logic/algorithms/dpll.py b/sympy/logic/algorithms/dpll.py > new file mode 100644 > index 0000000..77d92ed > --- /dev/null > +++ b/sympy/logic/algorithms/dpll.py > @@ -0,0 +1,49 @@ > +from sympy.core import Symbol > +from sympy.logic.boolalg import conjuncts, to_cnf > +from sympy.logic.inference import find_pure_symbol, find_unit_clause, pl_true > + > +def dpll_satisfiable(expr): > + """Check satisfiability of a propositional sentence. > + It returns a model rather than True when it succeeds > + >>> from sympy import symbols > + >>> A, B = symbols('AB') > + >>> dpll_satisfiable(A & ~B) > + {A: True, B: False} > + >>> dpll_satisfiable(A & ~A) > + False > + > + References: Implemented as described in http://aima.cs.berkeley.edu/ > + """ > + clauses = conjuncts(to_cnf(expr)) > + symbols = list(expr.atoms(Symbol)) > + return dpll(clauses, symbols, {}) > + > +def dpll(clauses, symbols, model): > + """See if the clauses are true in a partial model. > + http://en.wikipedia.org/wiki/DPLL_algorithm Add couple doctests > + """ > + unknown_clauses = [] ## clauses with an unknown truth value > + for c in clauses: > + val = pl_true(c, model) > + if val == False: > + return False > + if val != True: > + unknown_clauses.append(c) > + if not unknown_clauses: > + return model > + P, value = find_pure_symbol(symbols, unknown_clauses) > + if P: > + model.update({P: value}) > + syms = [x for x in symbols if x != P] > + return dpll(clauses, syms, model) > + P, value = find_unit_clause(clauses, model) > + if P: > + model.update({P: value}) > + syms = [x for x in symbols if x != P] > + return dpll(clauses, syms, model) > + P = symbols.pop() > + model_1, model_2 = model.copy(), model.copy() > + model_1.update({P: True}) > + model_2.update({P: False}) > + return (dpll(clauses, symbols, model_1) or > + dpll(clauses, symbols, model_2)) > \ No newline at end of file > diff --git a/sympy/logic/boolalg.py b/sympy/logic/boolalg.py > new file mode 100755 > index 0000000..1a57740 > --- /dev/null > +++ b/sympy/logic/boolalg.py > @@ -0,0 +1,155 @@ > +"""Boolean algebra module for SymPy""" > +from sympy.core import Basic, Function, sympify, Symbol > +from sympy.utilities import flatten > + > + > +class BooleanFunction(Function): > + """Boolean function is a function that lives in a boolean space > + It is used as base class for And, Or, Not, etc. > + """ > + pass > + > +class And(BooleanFunction): > + """Evaluates to True if all it's arguments are True, > + False otherwise""" Add couple doctests > + �...@classmethod > + def eval(cls, *args): > + if len(args) < 2: raise ValueError("And must have at least two > arguments") > + out_args = [] > + for i, arg in enumerate(args): # we iterate over a copy or args > + if isinstance(arg, bool): > + if not arg: return False > + else: continue > + out_args.append(arg) > + if len(out_args) == 0: return True > + if len(out_args) == 1: return out_args[0] > + sargs = sorted(flatten(out_args, cls=cls)) > + return Basic.__new__(cls, *sargs) > + > +class Or(BooleanFunction): Add couple doctests > + �...@classmethod > + def eval(cls, *args): > + if len(args) < 2: raise ValueError("And must have at least two > arguments") > + out_args = [] > + for i, arg in enumerate(args): # we iterate over a copy or args > + if isinstance(arg, bool): > + if arg: return True > + else: continue > + out_args.append(arg) > + if len(out_args) == 0: return False > + if len(out_args) == 1: return out_args[0] > + sargs = sorted(flatten(out_args, cls=cls)) > + return Basic.__new__(cls, *sargs) > + > +class Not(BooleanFunction): > + """De Morgan rules applied automatically, so Not is moved inward""" Add couple doctests > + �...@classmethod > + def eval(cls, *args): > + if len(args) > 1: > + return map(cls, args) > + arg = args[0] > + # apply De Morgan Rules > + if isinstance(arg, And): > + return Or( *[Not(a) for a in arg.args]) > + if isinstance(arg, Or): > + return And(*[Not(a) for a in arg.args]) > + if isinstance(arg, bool): return not arg > + if isinstance(arg, Not): > + if len(arg.args) == 1: return arg.args[0] > + return arg.args > + > +class Implies(BooleanFunction): Add couple doctests > + def __init__(self, *args, **kwargs): > + if len(args) != 2: raise ValueError("Wrong set of arguments") > + return super(BooleanFunction, self).__init__(*args, **kwargs) > + > +class Equivalent(BooleanFunction): > + pass > + > +### end class definitions. Some useful methods > + > +def conjuncts(expr): > + """Return a list of the conjuncts in the expr s. > + >>> from sympy import symbols > + >>> A, B = symbols('AB') > + >>> conjuncts(A & B) > + [A, B] > + >>> conjuncts(A | B) > + [Or(A, B)] > + """ > + if isinstance(expr, And): > + return list(expr.args) > + else: > + return [expr] > + > +def disjuncts(expr): > + """Return a list of the disjuncts in the sentence s. > + >>> from sympy import symbols > + >>> A, B = symbols('AB') > + >>> disjuncts(A | B) > + [A, B] > + >>> disjuncts(A & B) > + [And(A, B)] > + """ > + if isinstance(expr, Or): > + return list(expr.args) > + else: > + return [expr] > + > +def distribute_and_over_or(expr): > + """Given a sentence s consisting of conjunctions and disjunctions > + of literals, return an equivalent sentence in CNF. Add couple doctests > + """ > + if isinstance(expr, Or): > + expr = Or(*expr.args) > + if len(expr.args) == 0: > + return False > + if len(expr.args) == 1: > + return distribute_and_over_or(expr.args[0]) > + for arg in expr.args: > + if isinstance(arg, And): > + conj = arg > + break > + else: return type(expr)(*expr.args) > + others = [a for a in expr.args if a is not conj] > + if len(others) == 1: > + rest = others[0] > + else: > + rest = Or(*others) > + return And(*map(distribute_and_over_or, > + [Or(c, rest) for c in conj.args])) > + elif isinstance(expr, And): > + return And(*map(distribute_and_over_or, expr.args)) > + else: > + return expr > + > +def to_cnf(expr): > + """Convert a propositional logical sentence s to conjunctive normal form. > + That is, of the form ((A | ~B | ...) & (B | C | ...) & ...) Add couple doctests > + """ > + expr = sympify(expr) > + expr = eliminate_implications(expr) > + return distribute_and_over_or(expr) > + > +def eliminate_implications(expr): > + """Change >>, <<, and <=> into &, |, and ~. That is, return an Expr > + that is equivalent to s, but has only &, |, and ~ as logical operators. Add couple doctests > + """ > + expr = sympify(expr) > + if expr.is_Atom: return expr ## (Atoms are unchanged.) > + args = map(eliminate_implications, expr.args) > + a, b = args[0], args[-1] # TODO: multiple implications? > + if isinstance(expr, Implies): > + return Or(b, Not(a)) > + elif isinstance(expr, Equivalent): > + return And(Or(a, Not(b)), Or(b, Not(a))) > + else: > + return type(expr)(*args) > + > +def compile_rule(s): > + """Transforms a rule into a sympy expression > + A rule is a string of the form "symbol1 & symbol2 | ..." > + See sympy.assumptions.known_facts for examples of rules Add couple doctests > + """ > + import re > + return eval(re.sub(r'([a-zA-Z0-9_.]+)', r'Symbol("\1")', s), {'Symbol' : > Symbol}) > \ No newline at end of file > diff --git a/sympy/logic/inference.py b/sympy/logic/inference.py > new file mode 100755 > index 0000000..a638ee2 > --- /dev/null > +++ b/sympy/logic/inference.py > @@ -0,0 +1,119 @@ > +"""Inference in propositional logic""" > +from sympy.core import Symbol > +from sympy.logic.boolalg import And, Or, Not, Implies, Equivalent, > disjuncts, \ > + to_cnf > + > + > +def find_pure_symbol(symbols, unknown_clauses): > + """Find a symbol and its value if it appears only as a positive literal > + (or only as a negative) in clauses. > + >>> from sympy import symbols > + >>> A, B, C = symbols('ABC') > + >>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A]) > + (A, True) > + """ > + for sym in symbols: > + found_pos, found_neg = False, False > + for c in unknown_clauses: > + if not found_pos and sym in disjuncts(c): found_pos = True > + if not found_neg and Not(sym) in disjuncts(c): found_neg = True > + if found_pos != found_neg: return sym, found_pos > + return None, None > + > +def find_unit_clause(clauses, model): > + """A unit clause has only 1 variable that is not bound in the model. > + >>> from sympy import symbols > + >>> A, B, C = symbols('ABC') > + >>> find_unit_clause([A | B | C, B | ~C, A | ~B], {A:True}) > + (B, True) > + """ > + for clause in clauses: > + num_not_in_model = 0 > + for literal in disjuncts(clause): > + sym = literal_symbol(literal) > + if sym not in model: > + num_not_in_model += 1 > + P, value = sym, (isinstance(literal, Not)) > + if num_not_in_model == 1: > + return P, value > + return None, None > + > +def literal_symbol(literal): > + """The symbol in this literal (without the negation). > + >>> from sympy import Symbol > + >>> A = Symbol('A') > + >>> literal_symbol(A) > + A > + >>> literal_symbol(~A) > + A > + """ > + if isinstance(literal, Not): > + return literal.args[0] > + else: > + return literal > + > +def satisfiable(expr, algorithm="dpll"): > + """Check satisfiability of a propositional sentence. > + Returns a model when it succeeds > + > + Examples > + >>> from sympy import symbols > + >>> A, B = symbols('AB') > + >>> satisfiable(A & ~B) > + {A: True, B: False} > + >>> satisfiable(A & ~A) > + False > + """ > + expr = to_cnf(expr) > + if algorithm == "dpll": > + from sympy.logic.algorithms.dpll import dpll_satisfiable > + return dpll_satisfiable(expr) > + raise NotImplementedError > + > +def pl_true(expr, model={}): > + """Return True if the propositional logic expression is true in the > model, > + and False if it is false. If the model does not specify the value for > + every proposition, this may return None to indicate 'not obvious'; > + this may happen even when the expression is tautological. > + > + The model is implemented as a dict containing the pair symbol, boolean > value. > + > + Examples: > + >>> from sympy import symbols > + >>> A, B = symbols('AB') > + >>> pl_true( A & B, {A: True, B : True}) > + True > + """ > + args = expr.args > + if isinstance(expr, bool): > + return expr > + if expr.is_Atom: return model.get(expr) > + elif isinstance(expr, Not): > + p = pl_true(args[0], model) > + if p is None: return None > + else: return not p > + elif isinstance(expr, Or): > + result = False > + for arg in args: > + p = pl_true(arg, model) > + if p == True: return True > + if p == None: result = None > + return result > + elif isinstance(expr, And): > + result = True > + for arg in args: > + p = pl_true(arg, model) > + if p == False: return False > + if p == None: result = None > + return result > + p, q = args > + if isinstance(expr, Implies): > + return pl_true(Or(Not(p), q), model) > + pt = pl_true(p, model) > + if pt == None: return None > + qt = pl_true(q, model) > + if qt == None: return None > + if isinstance(expr, Equivalent): > + return pt == qt > + else: > + raise ValueError, "Illegal operator in logic expression" + str(exp) > \ No newline at end of file > diff --git a/sympy/logic/kb.py b/sympy/logic/kb.py > new file mode 100644 > index 0000000..267af91 > --- /dev/null > +++ b/sympy/logic/kb.py > @@ -0,0 +1,36 @@ > +from sympy.logic.boolalg import And, conjuncts, to_cnf > +from sympy.logic.inference import satisfiable > + > +class KB(object): > + pass > + > +class PropKB(KB): > + "A KB for Propositional Logic. Inefficient, with no indexing." Add couple doctests > + > + def __init__(self, sentence=None): > + self.clauses = [] > + if sentence: > + self.tell(sentence) > + > + def tell(self, sentence): > + "Add the sentence's clauses to the KB" > + self.clauses.extend(conjuncts(to_cnf(sentence))) > + > + def ask(self, query): > + """TODO: examples""" > + if len(self.clauses) == 0: return {} > + query_conjuncts = conjuncts(to_cnf(~query)) > + query_conjuncts.extend(self.clauses) > + return not satisfiable(And(*query_conjuncts)) > + > + def ask_generator(self, query): > + "Yield the empty substitution if KB implies query; else False" > + if not tt_entails(Expr('&', *self.clauses), query): > + return > + yield {} > + > + def retract(self, sentence): > + "Remove the sentence's clauses from the KB" > + for c in conjuncts(to_cnf(sentence)): > + if c in self.clauses: > + self.clauses.remove(c) > \ No newline at end of file > diff --git a/sympy/logic/tests/test_boolalg.py > b/sympy/logic/tests/test_boolalg.py > new file mode 100755 > index 0000000..e4f1c32 > --- /dev/null > +++ b/sympy/logic/tests/test_boolalg.py > @@ -0,0 +1,110 @@ > +from sympy.logic.boolalg import And, Or, Not, Implies, Equivalent, to_cnf, \ > + eliminate_implications, distribute_and_over_or > +from sympy import symbols > +from sympy.utilities.pytest import raises > + > +def test_overloading(): > + """Test that |, & are overloaded as expected""" > + A, B, C = symbols('ABC') > + assert A & B == And(A, B) > + assert A | B == Or(A, B) > + assert (A & B) | C == Or(And(A, B), C) > + assert A >> B == Implies(A, B) > + assert A << B == Implies(B, A) > + assert ~A == Not(A) > + > +def test_bool_unary(): > + """Test boolean functions with one argument > + Only Not is well defined in this case. > + All other functions should raise a ValueError > + """ > + A = symbols('A') > + assert Not(True ) == False > + assert Not(False) == True > + raises(ValueError, "And(True)") > + raises(ValueError, "And(False)") > + raises(ValueError, "And(A)") > + raises(ValueError, "Or(True)") > + raises(ValueError, "Or(False)") > + raises(ValueError, "Or(A)") > + > +def test_And_bool_2(): > + """Test And with two boolean arguments""" > + assert And(True, True ) == True > + assert And(True, False) == False > + assert And(False, False) == False > + > +def test_Or_bool(): > + assert Or(True, True ) == True > + assert Or(True, False) == True > + assert Or(False, False) == False > + > +def test_Not_bool(): > + assert Not(True, True ) == [False, False] > + assert Not(True, False) == [False, True ] > + assert Not(False,False) == [True, True ] > + > +def test_Implies(): > + A, B, C = symbols('ABC') > + raises(ValueError, "Implies()") > + raises(ValueError, "Implies(A)") > + raises(ValueError, "Implies(A, B, C)") > + > +def test_bool_symbol(): > + """Test that mixing symbols with boolean values > + works as expected""" > + A, B, C = symbols('ABC') > + assert And(A, True) == A > + assert And(A, True, True) == A > + assert And(A, False) == False > + assert And(A, True, False) == False > + assert Or(A, True) == True > + assert Or(A, False) == A > + > +""" > +we test for axioms of boolean algebra > +see http://en.wikipedia.org/wiki/Boolean_algebra_(structure) > +""" > + > +def test_commutative(): > + """Test for commutativity of And and Not""" > + A, B = symbols('AB') > + assert A & B == B & A > + assert A | B == B | A > + > +def test_and_associativity(): > + """Test for associativity of And""" > + A, B, C = symbols('ABC') > + assert (A & B) & C == A & (B & C) > + > +def test_or_assicativity(): > + A, B, C = symbols('ABC') > + assert ((A | B) | C) == (A | (B | C)) > + > +def test_double_negation(): > + a = symbols('a') > + assert ~(~a) == a > + > +def test_De_Morgan(): > + A, B = symbols('AB') > + assert ~(A & B) == (~A) | (~B) > + assert ~(A | B) == (~A) & (~B) > + > + > +# test methods > +def test_eliminate_implications(): > + A, B, C = symbols('ABC') > + assert eliminate_implications( A >> B) == (~A) | B > + assert eliminate_implications(A >> (C >>Not(B))) \ > + == Or(Or(Not(B), Not(C)), Not(A)) > + > +def test_distribute(): > + A, B, C = symbols('ABC') > + assert distribute_and_over_or(Or(And(A, B), C)) == And(Or(A, C), Or(B, > C)) > + > + > +def test_to_cnf(): > + A, B, C = symbols('ABC') > + assert to_cnf(Not(Or(B, C))) == And(Not(B), Not(C)) > + assert to_cnf(Equivalent(A, Or(B,C))) == \ > + And(Or(Not(B), A), Or(Not(C), A), Or(B, C, Not(A))) > diff --git a/sympy/logic/tests/test_inference.py > b/sympy/logic/tests/test_inference.py > new file mode 100755 > index 0000000..6667e15 > --- /dev/null > +++ b/sympy/logic/tests/test_inference.py > @@ -0,0 +1,46 @@ > +from sympy import symbols > +from sympy.logic.boolalg import And, Not, Or, Equivalent, Implies, to_cnf > +from sympy.logic.inference import pl_true, satisfiable > +from sympy.logic.algorithms.dpll import dpll > +from sympy.logic.kb import PropKB > + > + > +def test_satisfiable(): > + A, B, C = symbols('ABC') > + assert satisfiable( A & ~A ) == False > + assert satisfiable( A & ~B ) == {A: True, B: False} > + assert satisfiable( A & B & C ) == {A: True, B: True, C: True} > + assert satisfiable( A | B ) in ({A: True}, {B: True}) > + > +def test_satisfiable_1(): > + """We prove expr entails alpha proving expr & ~B is unsatisfiable""" > + A, B, C = symbols('ABC') > + assert satisfiable(A & (A >> B) & ~B) == False > + > +def test_pl_true(): > + A, B, C = symbols('ABC') > + assert pl_true( And(A, B), {A : True, B : True}) == True > + assert pl_true( Or(A, B), {A : True}) == True > + assert pl_true( Or(A, B), {B : True}) == True > + > + # test for false > + assert pl_true ( And(A, B), {A: False, B: False}) == False > + assert pl_true ( And(A, B), {A: False}) == False > + assert pl_true ( And(A, B), {B: False}) == False > + assert pl_true ( Or(A, B), {A: False, B: False}) == False > + > +def test_PropKB(): > + A, B, C = symbols('ABC') > + kb = PropKB() > + kb.tell(A) > + kb.tell(A >> B) > + kb.tell(B >> C) > + assert kb.ask(B) == True > + assert kb.ask(C) == True > + "TODO: test for api: .clauses etc" > + > +def test_propKB_tolerant(): > + """"Test that it is tolerant to bad input""" > + kb = PropKB() > + A, B, C = symbols('ABC') > + assert kb.ask(B) == {} > -- > 1.6.1.2 > > > > > --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "sympy-patches" group. To post to this group, send email to sympy-patches@googlegroups.com To unsubscribe from this group, send email to sympy-patches+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/sympy-patches?hl=en -~----------~----~----~----~------~----~------~--~---