The following are a series of patches that implement the new assumption system, which consists of:
- preparatory stuff (patches 1-5) - a logic module (much cleaner than the old one) - new style-assumptions (very simple objects) - query module, which substitutes the .is_* syntax
I tried hard to separate these 3 functionalities as much as possible.
This is more an initial implementation from whom we can star to work rather than a finished product, so in case you have any objections, we can start discussing.
Thanks for the review,
- Fabian
pd: patch 3 was already approved, just that i did not push it in.
variations is a routine that calculates all possible variations of size n (with or without repetition) of a given set. --- doc/src/modules.txt | 1 + doc/src/modules/utilities/index.txt | 11 ++++++++ doc/src/modules/utilities/iterables.txt | 39 +++++++++++++++++++++++++++++++ sympy/utilities/iterables.py | 36 ++++++++++++++++++++++++++++ sympy/utilities/tests/test_iterables.py | 6 ++++- 5 files changed, 92 insertions(+), 1 deletions(-) create mode 100644 doc/src/modules/utilities/index.txt create mode 100644 doc/src/modules/utilities/iterables.txt
diff --git a/doc/src/modules.txt b/doc/src/modules.txt index fca4eb6..6c46683 100644 --- a/doc/src/modules.txt +++ b/doc/src/modules.txt @@ -31,6 +31,7 @@ access any SymPy module, or use this contens: modules/statistics.txt modules/concrete.txt modules/solvers.txt + modules/utilities/index.txt
Contributions to docs --------------------- diff --git a/doc/src/modules/utilities/index.txt b/doc/src/modules/utilities/index.txt new file mode 100644 index 0000000..3e1e692 --- /dev/null +++ b/doc/src/modules/utilities/index.txt @@ -0,0 +1,11 @@ +Utilities +========= + +This module contains some general purpose utilities that are used across SymPy + +Contents: + +.. toctree:: + :maxdepth: 2 + + iterables.txt \ No newline at end of file diff --git a/doc/src/modules/utilities/iterables.txt b/doc/src/modules/utilities/iterables.txt new file mode 100644 index 0000000..023a3bc --- /dev/null +++ b/doc/src/modules/utilities/iterables.txt @@ -0,0 +1,39 @@ +Iterables +========= + +cartes +------ + +Returns the cartesian product of two sequences + +Examples:: + >>> from sympy.utilities.iterables import cartes + >>> cartes([1,2,3], ['a', 'b', 'c']) + [[1, 'a'], + [1, 'b'], + [1, 'c'], + [2, 'a'], + [2, 'b'], + [2, 'c'], + [3, 'a'], + [3, 'b'], + [3, 'c']] + + + +variations +---------- + +variations(seq, n) Returns all the variations of the list of size n. + +Has an optional third argument. Must be a boolean value and makes the method +return the variations with repetition if set to True, or the variations +without repetition if set to False. + +Examples:: + >>> from sympy.utilities.iterables import variations + >>> variations([1,2,3], 2) + [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]] + >>> variations([1,2,3], 2, True) + [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3], [3, 1], [3, 2], [3, 3]] + diff --git a/sympy/utilities/iterables.py b/sympy/utilities/iterables.py index 6179622..24ebcd0 100644 --- a/sympy/utilities/iterables.py +++ b/sympy/utilities/iterables.py @@ -173,3 +173,39 @@ def recursion(result, M, k): for elem in recursion([item], M[i + 1:], k - 1): yield elem
+ +def cartes(seq0, seq1, modus='pair'): + """Return the cartesian product of two sequences """ + if modus == 'pair': + return [[item0, item1] for item0 in seq0 for item1 in seq1] + elif modus == 'triple': + return [item0 + [item1] for item0 in seq0 for item1 in seq1] + +def variations(seq, n, repetition=False): + """Returns all the variations of the list of size n. + + variations(seq, n, True) will return all the variations of the list of + size n with repetitions + + variations(seq, n, False) will return all the variations of the list of + size n without repetitions + """ + def setrep(seq): # remove sets with duplicates (repetition is relevant) + def delrep(seq): # remove duplicates while maintaining order + result = [] + for item in seq: + if item not in result: + result.append(item) + return result + return [item for item in seq if item == delrep(item)] + + if n == 1: + return [[item] for item in seq] + result = range(len(seq)) + cartesmodus = 'pair' + for i in range(n-1): + result = cartes(result, range(len(seq)), cartesmodus) + if not repetition: + result = setrep(result) + cartesmodus = 'triple' + return [[seq[index] for index in indices] for indices in result] diff --git a/sympy/utilities/tests/test_iterables.py b/sympy/utilities/tests/test_iterables.py index 1eea497..959c900 100644 --- a/sympy/utilities/tests/test_iterables.py +++ b/sympy/utilities/tests/test_iterables.py @@ -1,6 +1,6 @@ from sympy import symbols from sympy.utilities.iterables import postorder_traversal, \ - preorder_traversal, flatten, subsets + preorder_traversal, flatten, subsets, variations
Function get_class returns a class from a given string. It makes use of get_mod_func
Implemented after a function with same name in django's codebase --- sympy/utilities/source.py | 30 ++++++++++++++++++++++++++++++ sympy/utilities/tests/test_source.py | 8 ++++++++ 2 files changed, 38 insertions(+), 0 deletions(-) create mode 100644 sympy/utilities/tests/test_source.py
diff --git a/sympy/utilities/source.py b/sympy/utilities/source.py index 56ca7f7..213d3d8 100644 --- a/sympy/utilities/source.py +++ b/sympy/utilities/source.py @@ -10,3 +10,33 @@ def source(object): """ print 'In file: %s' % inspect.getsourcefile(object) print inspect.getsource(object) + +def get_class(lookup_view): + """ + Convert a string version of a class name to the object. + + For example, get_class('sympy.core.Basic') will return + class Basic located in module sympy.core + """ + if isinstance(lookup_view, str): + # Bail early for non-ASCII strings (they can't be functions). + lookup_view = lookup_view.encode('ascii') + mod_name, func_name = get_mod_func(lookup_view) + if func_name != '': + lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name) + if not callable(lookup_view): + raise AttributeError("'%s.%s' is not a callable." % (mod_name, func_name)) + return lookup_view + +def get_mod_func(callback): + """ + splits the string path to a class into a string path to the module + and the name of the class. For example: + >>> get_mod_func('sympy.core.basic.Basic') + ('sympy.core.basic', 'Basic') + """ + try: + dot = callback.rindex('.') + except ValueError: + return callback, '' + return callback[:dot], callback[dot+1:] \ No newline at end of file diff --git a/sympy/utilities/tests/test_source.py b/sympy/utilities/tests/test_source.py new file mode 100644 index 0000000..0716d46 --- /dev/null +++ b/sympy/utilities/tests/test_source.py @@ -0,0 +1,8 @@ +from sympy.utilities.source import get_mod_func, get_class + +def test_get_mod_func(): + assert get_mod_func('sympy.core.basic.Basic') == ('sympy.core.basic', 'Basic') + +def test_get_class(): + _basic = get_class('sympy.core.basic.Basic') + assert _basic.__name__ == 'Basic' \ No newline at end of file -- 1.6.1.2
It was specially annoying that you could not return list or tuples.
I don't know the reason of this behaviour, fortunately changing it did not break any tests.
I added a test for this --- sympy/core/function.py | 17 ++++++----------- sympy/core/tests/test_functions.py | 7 +++++++ 2 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/sympy/core/function.py b/sympy/core/function.py index aae869d..8331751 100644 --- a/sympy/core/function.py +++ b/sympy/core/function.py @@ -138,18 +138,13 @@ def __new__(cls, *args, **options): # up to here. if options.get('evaluate') is False: return Basic.__new__(cls, *args, **options) - r = cls.eval(*args) - if isinstance(r, Basic): + evaluated = cls.eval(*args) + if evaluated is not None: return evaluated + # Just undefined functions have nargs == None + if not cls.nargs and hasattr(cls, 'undefined_Function'): + r = Basic.__new__(cls, *args, **options) + r.nargs = len(args) return r - elif r is None: - # Just undefined functions have nargs == None - if not cls.nargs and hasattr(cls, 'undefined_Function'): - r = Basic.__new__(cls, *args, **options) - r.nargs = len(args) - return r - pass - elif not isinstance(r, tuple): - args = (r,) return Basic.__new__(cls, *args, **options)
This permits to flatten with respect to a given class (see docstring). This will be used in the logic module to denest classes of the same type (but I believe it will be useful also in other places) --- sympy/utilities/iterables.py | 25 +++++++++++++++++++++---- sympy/utilities/tests/test_iterables.py | 6 ++++++ 2 files changed, 27 insertions(+), 4 deletions(-)
Before of this, sympy would convert True --> S.One, False --> S.Zero, but with the logic module with classes inheriting from basic, I need to manipulate boolean expressions, so converting booleans to S.One and S.Zero is in my oppinion wrong because of conceptual reasons and efficiency.
The only module that used this behaviour was functions.elementary.piecewise, and was adopted to use the new one. --- sympy/core/sympify.py | 4 ++-- sympy/functions/elementary/piecewise.py | 18 +++++++++++------- sympy/printing/ccode.py | 2 +- sympy/printing/latex.py | 2 +- sympy/printing/pretty/pretty.py | 2 +- sympy/utilities/decorator.py | 3 ++- 6 files changed, 18 insertions(+), 13 deletions(-)
diff --git a/sympy/core/sympify.py b/sympy/core/sympify.py index 8da69d2..b00961b 100644 --- a/sympy/core/sympify.py +++ b/sympy/core/sympify.py @@ -57,6 +57,8 @@ def sympify(a, locals=None, convert_xor=True): return a if isinstance(a, BasicType): return a + elif isinstance(a, bool): + return a elif isinstance(a, (int, long)): return Integer(a) elif isinstance(a, (float, decimal.Decimal)): @@ -68,8 +70,6 @@ def sympify(a, locals=None, convert_xor=True): if ireal + iimag*1j == a: return ireal + iimag*S.ImaginaryUnit return real + S.ImaginaryUnit * imag - elif isinstance(a, bool): - raise NotImplementedError("bool support") elif isinstance(a, (list,tuple,set)): return type(a)([sympify(x) for x in a])
diff --git a/sympy/functions/elementary/piecewise.py b/sympy/functions/elementary/piecewise.py index d5afef7..3abbb1f 100644 --- a/sympy/functions/elementary/piecewise.py +++ b/sympy/functions/elementary/piecewise.py @@ -147,7 +147,7 @@ def _eval_interval(self, sym, a, b): # - eg x < 1, x < 3 -> [oo,1],[1,3] instead of [oo,1],[oo,3] # 3) Sort the intervals to make it easier to find correct exprs for expr, cond in self.args: - if cond.is_Number: + if isinstance(cond, bool) or cond.is_Number: if cond: default = expr break @@ -203,15 +203,19 @@ def _eval_derivative(self, s): def _eval_subs(self, old, new): if self == old: return new - return Piecewise(*[(e._eval_subs(old, new), c._eval_subs(old, new)) \ - for e, c in self.args]) + new_args = [] + for e, c in self.args: + if isinstance(c, bool): + new_args.append((e._eval_subs(old, new), c)) + else: + new_args.append((e._eval_subs(old, new), c._eval_subs(old, new))) + return Piecewise( *new_args )
@classmethod def __eval_cond(cls, cond): """Returns S.One if True, S.Zero if False, or None if undecidable.""" - if type(cond) == bool or cond.is_number: - return sympify(bool(cond)) - if cond.args[0].is_Number and cond.args[1].is_Number: - return sympify(bool(cond)) + if type(cond) == bool or cond.is_number or (cond.args[0].is_Number and cond.args[1].is_Number): + if cond: return S.One + return S.Zero return None
diff --git a/sympy/printing/ccode.py b/sympy/printing/ccode.py index a1341b5..7a6ccf6 100644 --- a/sympy/printing/ccode.py +++ b/sympy/printing/ccode.py @@ -38,7 +38,7 @@ def _print_Piecewise(self, expr): ecpairs = ["(%s) {\n%s\n}\n" % (self._print(c), self._print(e)) \ for e, c in expr.args[:-1]] last_line = "" - if expr.args[-1].cond is S.One: + if expr.args[-1].cond == True: last_line = "else {\n%s\n}" % self._print(expr.args[-1].expr) else: ecpairs.append("(%s) {\n%s\n" % \ diff --git a/sympy/printing/latex.py b/sympy/printing/latex.py index 6f2b089..02cdd00 100644 --- a/sympy/printing/latex.py +++ b/sympy/printing/latex.py @@ -521,7 +521,7 @@ def _print_Relational(self, expr): def _print_Piecewise(self, expr): ecpairs = [r"%s & for %s" % (self._print(e), self._print(c)) \ for e, c in expr.args[:-1]] - if expr.args[-1].cond is S.One: + if expr.args[-1].cond == True: ecpairs.append(r"%s & \textrm{otherwise}" % \ self._print(expr.args[-1].expr)) else: diff --git a/sympy/printing/pretty/pretty.py b/sympy/printing/pretty/pretty.py index 5065d6c..19ffff2 100644 --- a/sympy/printing/pretty/pretty.py +++ b/sympy/printing/pretty/pretty.py @@ -318,7 +318,7 @@ def _print_Piecewise(self, pexpr): P = {} for n, ec in enumerate(pexpr.args): P[n,0] = self._print(ec.expr) - if ec.cond is S.One: + if ec.cond == True: P[n,1] = prettyForm('otherwise') else: P[n,1] = prettyForm(*prettyForm('for ').right(self._print(ec.cond))) diff --git a/sympy/utilities/decorator.py b/sympy/utilities/decorator.py index 1d022d6..489f781 100644 --- a/sympy/utilities/decorator.py +++ b/sympy/utilities/decorator.py @@ -39,6 +39,8 @@ def threaded_proxy(func): def threaded_decorator(expr, *args, **kwargs): if isinstance(expr, Matrix): return expr.applyfunc(lambda f: func(f, *args, **kwargs)) + elif isinstance(expr, bool): + return expr elif hasattr(expr, '__iter__'): return expr.__class__([ func(f, *args, **kwargs) for f in expr ]) else: @@ -47,7 +49,6 @@ def threaded_decorator(expr, *args, **kwargs): if isinstance(expr, Relational): lhs = func(expr.lhs, *args, **kwargs) rhs = func(expr.rhs, *args, **kwargs) - return Relational(lhs, rhs, expr.rel_op) elif expr.is_Add and use_add: return Add(*[ func(f, *args, **kwargs) for f in expr.args ]) -- 1.6.1.2
The old query system (.is_* properties) has some limitations:
1. Can't assume relations. It is not possible to specify relations between different objects. For example, you can assume that x>0 and y>0, but you can't assume x>y
2. Verbose code splitted across multiple classes. Each class must override query methods (.is_*).
3. Extensibility. To add new assumptions you have to add code to the core
The assumption system implemented in this series of patches relies on these principles:
1. Assumptions are separate from objects. Assumptions are instances of Assume, which is a class that holds a reference to the expression and to the 'key' (what it assumes)
2. Queries out of the core. No more .is_* methods. Specific queries are implemented as subclasses of QueryHandler, and the query function calls the appropriate subclass of QueryHandler
query -> QueryHandler -> QueryNegativeHandler -> QueryIntegerHandler -> QueryBoundedHandler ... That way, creating new queries is a matter of subclassing QueryHandler and override the apropriate methods.
PD: note that this query module cannot use relations (yet), but at least the infraestructure is ready to support them. --- sympy/__init__.py | 1 + sympy/query/__init__.py | 130 ++++++++ sympy/query/handlers/__init__.py | 103 +++++++ sympy/query/handlers/calculus.py | 118 ++++++++ sympy/query/handlers/ntheory.py | 114 +++++++ sympy/query/handlers/order.py | 156 ++++++++++ sympy/query/handlers/sets.py | 255 ++++++++++++++++ sympy/query/tests/test_query.py | 606 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 1483 insertions(+), 0 deletions(-) create mode 100644 sympy/query/__init__.py create mode 100644 sympy/query/handlers/__init__.py create mode 100644 sympy/query/handlers/calculus.py create mode 100644 sympy/query/handlers/ntheory.py create mode 100644 sympy/query/handlers/order.py create mode 100644 sympy/query/handlers/sets.py create mode 100644 sympy/query/tests/test_query.py
diff --git a/sympy/__init__.py b/sympy/__init__.py index d5b33fc..3f08748 100644 --- a/sympy/__init__.py +++ b/sympy/__init__.py @@ -34,6 +34,7 @@ def __sympy_debug(): from geometry import * from utilities import * from integrals import * +from query import query # This module is slow to import: #from physics import units from plotting import Plot, textplot diff --git a/sympy/query/__init__.py b/sympy/query/__init__.py new file mode 100644 index 0000000..6d7f228 --- /dev/null +++ b/sympy/query/__init__.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import inspect +from sympy.core import Symbol, sympify +from sympy.utilities.source import get_class +from sympy.assumptions.known_facts import known_facts_dict +from sympy.logic.kb import PropKB +from sympy.logic.boolalg import compile_rule + +def query(expr, *args, **kwargs): + """ + 1. We build a knowledge base + """ + assumptions = kwargs.pop('assumptions', []) + if not isinstance(assumptions, (list, tuple)): + assumptions = [assumptions] + + key, value = kwargs.popitem() + expr = sympify(expr) + if len(kwargs) > 0: + # split multiple queries + k = {key: value} + return query(expr, assumptions=assumptions, **k) and \ + query(expr, assumptions=assumptions, **kwargs) + if not value: # only True queries from now on (e.g. prime=True) + k = {key: True} + result = query(expr, assumptions=assumptions, **k) + if result is not None: + return not result + return + + # direct resolution method, no logic + resolutor = get_class(handlers_dict[key])(expr, key, assumptions) + if resolutor.resolution is not None: + return resolutor.resolution + + # use logic inference + if not expr.is_Atom: return + kb = PropKB() + # add the knowledge base facts related to assumptions + for assump in assumptions: + if expr.has(assump.expr): + if assump.value: + kb.tell(Symbol(assump.key)) + else: + kb.tell(~Symbol(assump.key)) + for s in known_facts_dict.get(assump.key, []): + if assump.value: + kb.tell(compile_rule(s)) + else: + kb.tell(~compile_rule(s)) + + if kb.ask(Symbol(key)): return True + if kb.ask(~Symbol(key)): return False + + # try to prove some equivalent fact + equiv = known_facts_dict.get(key, []) + #if equiv is None: return + for s in equiv: + rule = compile_rule(s) + if kb.ask(rule): + return True + elif kb.ask(~rule): + return False + + # try some depht-first search, we collect all info in the + # knowledge base + symbols = [] + for clause in kb.clauses: + for s in clause.atoms(Symbol): + if str(s) == key: continue + if str(s) in [x.key for x in assumptions]: continue + if s not in symbols: + symbols.append(s) + + for sym in symbols: + if kb.ask(sym) == True: + for s in known_facts_dict.get(str(sym), []): + kb.tell(compile_rule(s)) + + if kb.ask(Symbol(key)): return True + if kb.ask(~Symbol(key)): return False + +# query_dict tells us what query handler we should use +# for a particular key +handlers_dict = { + 'bounded' : 'sympy.query.handlers.calculus.QueryBoundedHandler', + 'commutative' : 'sympy.query.handlers.QueryCommutativeHandler', + 'comparable' : 'sympy.query.handlers.QueryComparableHadler', + 'complex' : 'sympy.query.handlers.sets.QueryComplexHandler', + 'composite' : 'sympy.query.handlers.ntheory.QueryCompositeHandler', + 'even' : 'sympy.query.handlers.ntheory.QueryEvenHandler', + 'extended_real' : 'sympy.query.handlers.sets.QueryExtendedRealHandler', + 'finite' : 'sympy.query.handlers.QueryFiniteHandler', + 'imaginary' : 'sympy.query.handlers.sets.QueryImaginaryHandler', + 'infinitesimal' : 'sympy.query.handlers.QueryInfinitesimalHandler', + 'integer' : 'sympy.query.handlers.sets.QueryIntegerHandler', + 'irrational' : 'sympy.query.handlers.sets.QueryIrrationalHandler', + 'rational' : 'sympy.query.handlers.sets.QueryRationalHandler', + 'nonnegative' : 'sympy.query.handlers.order.QueryNonNegativeHandler', + 'noninteger' : 'sympy.query.handlers.sets.QueryNonIntegerHandler', + 'nonpositive' : 'sympy.query.handlers.order.QueryNonPositiveHandler', + 'nonzero' : 'sympy.query.handlers.order.QueryNonZeroHandler', + 'negative' : 'sympy.query.handlers.order.QueryNegativeHandler', + 'positive' : 'sympy.query.handlers.order.QueryPositiveHandler', + 'prime' : 'sympy.query.handlers.ntheory.QueryPrimeHandler', + 'real' : 'sympy.query.handlers.sets.QueryRealHandler', + 'odd' : 'sympy.query.handlers.ntheory.QueryOddHandler', + 'unbounded' : 'sympy.query.handlers.calculus.QueryUnboundedHandler', + 'zero' : 'sympy.query.handlers.order.QueryZeroHandler' +} + +class QueryHandler(object): + """Base class that all Query Handlers must inherit""" + + facts = [] + resolution = None + + def __init__(self, expr, key, assumptions=None): + """ + TODO + """ + self.expr = expr + self.key = key + self.assumptions = assumptions + + mro = inspect.getmro(type(self.expr)) + for subclass in mro: + if hasattr(self, subclass.__name__): + self.resolution = getattr(self, subclass.__name__)() + break diff --git a/sympy/query/handlers/__init__.py b/sympy/query/handlers/__init__.py new file mode 100644 index 0000000..8945a0b --- /dev/null +++ b/sympy/query/handlers/__init__.py @@ -0,0 +1,103 @@ + +from sympy.query import query, QueryHandler + + +"""Facts must only have one level of indirection to avoid infinite recursion""" + +class CommonHandler(QueryHandler): + """Defines some useful methods common to most Handlers """ + def Symbol(self): + if self.assumptions is None: + return None + for assump in self.assumptions: + if assump.expr == self.expr and assump.key == self.key: + return assump.value + return None + + def NaN(self): + return False + + +class QueryCommutativeHandler(CommonHandler): + def Basic(self): + for a in self.expr.args: + if query(a, commutative=True, assumptions=self.assumptions) == False: + return False + else: + return True + + def NaN(self): + return True + +class QueryFiniteHandler(CommonHandler): + + def Mul(self): + for arg in self.expr.args: + _result = query(arg, finite=True, assumptions=self.assumptions) + if not _result: + return _result + return True + + Add = Mul + + def Number(self): + return True + + def Infinity(self): + return False + + def NegativeInfinity(self): + return False + + def NumberSymbol(self): + return True + + def ImaginaryUnit(self): + return True + +class QueryInfinitesimalHandler(CommonHandler): + + def Mul(self): + for arg in self.expr.args: + _result = query(arg, infinitesimal=True, assumptions=self.assumptions) + if not _result: + return _result + return True + + Add = Mul + + def Number(self): + return self.expr == 0 + + def Pi(self): + return False + + def Exp1(self): + return False + + def ImaginaryUnit(self): + return False + +class
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.
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
- - + # ******************* + # * 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 + """ + 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""" + @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): + @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""" + @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): + 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. + """ + 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 | ...) & ...) + """ + 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 +
> variations is a routine that calculates all possible variations
> of size n (with or without repetition) of a given set.
> ---
> doc/src/modules.txt | 1 +
> doc/src/modules/utilities/index.txt | 11 ++++++++
> doc/src/modules/utilities/iterables.txt | 39 +++++++++++++++++++++++++++++++
> sympy/utilities/iterables.py | 36 ++++++++++++++++++++++++++++
> sympy/utilities/tests/test_iterables.py | 6 ++++-
> 5 files changed, 92 insertions(+), 1 deletions(-)
> create mode 100644 doc/src/modules/utilities/index.txt
> create mode 100644 doc/src/modules/utilities/iterables.txt
> diff --git a/doc/src/modules.txt b/doc/src/modules.txt
> index fca4eb6..6c46683 100644
> --- a/doc/src/modules.txt
> +++ b/doc/src/modules.txt
> @@ -31,6 +31,7 @@ access any SymPy module, or use this contens:
> modules/statistics.txt
> modules/concrete.txt
> modules/solvers.txt
> + modules/utilities/index.txt
> Contributions to docs
> ---------------------
> diff --git a/doc/src/modules/utilities/index.txt b/doc/src/modules/utilities/index.txt
> new file mode 100644
> index 0000000..3e1e692
> --- /dev/null
> +++ b/doc/src/modules/utilities/index.txt
> @@ -0,0 +1,11 @@
> +Utilities
> +=========
> +
> +This module contains some general purpose utilities that are used across SymPy
> +
> +Contents:
> +
> +.. toctree::
> + :maxdepth: 2
> +
> + iterables.txt
> \ No newline at end of file
> diff --git a/doc/src/modules/utilities/iterables.txt b/doc/src/modules/utilities/iterables.txt
> new file mode 100644
> index 0000000..023a3bc
> --- /dev/null
> +++ b/doc/src/modules/utilities/iterables.txt
> @@ -0,0 +1,39 @@
> +Iterables
> +=========
> +
> +cartes
> +------
> +
> +Returns the cartesian product of two sequences
> +
> +Examples::
> + >>> from sympy.utilities.iterables import cartes
> + >>> cartes([1,2,3], ['a', 'b', 'c'])
> + [[1, 'a'],
> + [1, 'b'],
> + [1, 'c'],
> + [2, 'a'],
> + [2, 'b'],
> + [2, 'c'],
> + [3, 'a'],
> + [3, 'b'],
> + [3, 'c']]
> +
> +
> +
> +variations
> +----------
> +
> +variations(seq, n) Returns all the variations of the list of size n.
> +
> +Has an optional third argument. Must be a boolean value and makes the method
> +return the variations with repetition if set to True, or the variations
> +without repetition if set to False.
> +
> +Examples::
> + >>> from sympy.utilities.iterables import variations
> + >>> variations([1,2,3], 2)
> + [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
> + >>> variations([1,2,3], 2, True)
> + [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3], [3, 1], [3, 2], [3, 3]]
> +
> diff --git a/sympy/utilities/iterables.py b/sympy/utilities/iterables.py
> index 6179622..24ebcd0 100644
> --- a/sympy/utilities/iterables.py
> +++ b/sympy/utilities/iterables.py
> @@ -173,3 +173,39 @@ def recursion(result, M, k):
> for elem in recursion([item], M[i + 1:], k - 1):
> yield elem
> +
> +def cartes(seq0, seq1, modus='pair'):
> + """Return the cartesian product of two sequences """
> + if modus == 'pair':
> + return [[item0, item1] for item0 in seq0 for item1 in seq1]
> + elif modus == 'triple':
> + return [item0 + [item1] for item0 in seq0 for item1 in seq1]
> +
> +def variations(seq, n, repetition=False):
> + """Returns all the variations of the list of size n.
> +
> + variations(seq, n, True) will return all the variations of the list of
> + size n with repetitions
> +
> + variations(seq, n, False) will return all the variations of the list of
> + size n without repetitions
> + """
> + def setrep(seq): # remove sets with duplicates (repetition is relevant)
> + def delrep(seq): # remove duplicates while maintaining order
> + result = []
> + for item in seq:
> + if item not in result:
> + result.append(item)
> + return result
> + return [item for item in seq if item == delrep(item)]
> +
> + if n == 1:
> + return [[item] for item in seq]
> + result = range(len(seq))
> + cartesmodus = 'pair'
> + for i in range(n-1):
> + result = cartes(result, range(len(seq)), cartesmodus)
> + if not repetition:
> + result = setrep(result)
> + cartesmodus = 'triple'
> + return [[seq[index] for index in indices] for indices in result]
> diff --git a/sympy/utilities/tests/test_iterables.py b/sympy/utilities/tests/test_iterables.py
> index 1eea497..959c900 100644
> --- a/sympy/utilities/tests/test_iterables.py
> +++ b/sympy/utilities/tests/test_iterables.py
> @@ -1,6 +1,6 @@
> from sympy import symbols
> from sympy.utilities.iterables import postorder_traversal, \
> - preorder_traversal, flatten, subsets
> + preorder_traversal, flatten, subsets, variations
On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote:
> The following are a series of patches that implement the new assumption system, which > consists of:
> - preparatory stuff (patches 1-5) > - a logic module (much cleaner than the old one) > - new style-assumptions (very simple objects) > - query module, which substitutes the .is_* syntax
> I tried hard to separate these 3 functionalities as much as possible.
> This is more an initial implementation from whom we can star to work rather than a > finished product, so in case you have any objections, we can start discussing.
> Thanks for the review,
Could you also push this to some branch to github? I'll play with it.
Ondrej Certik wrote:
> On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote:
>> The following are a series of patches that implement the new assumption system, which
>> consists of:
>> - preparatory stuff (patches 1-5)
>> - a logic module (much cleaner than the old one)
>> - new style-assumptions (very simple objects)
>> - query module, which substitutes the .is_* syntax
>> I tried hard to separate these 3 functionalities as much as possible.
>> This is more an initial implementation from whom we can star to work rather than a
>> finished product, so in case you have any objections, we can start discussing.
>> Thanks for the review,
> Could you also push this to some branch to github? I'll play with it.
Fabian Pedregosa wrote: > Ondrej Certik wrote: >> On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote: >>> The following are a series of patches that implement the new assumption system, which >>> consists of:
>>> - preparatory stuff (patches 1-5) >>> - a logic module (much cleaner than the old one) >>> - new style-assumptions (very simple objects) >>> - query module, which substitutes the .is_* syntax
>>> I tried hard to separate these 3 functionalities as much as possible.
>>> This is more an initial implementation from whom we can star to work rather than a >>> finished product, so in case you have any objections, we can start discussing.
>>> Thanks for the review, >> Could you also push this to some branch to github? I'll play with it.
On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote:
> This permits to flatten with respect to a given class (see docstring).
> This will be used in the logic module to denest classes of the same type
> (but I believe it will be useful also in other places)
> ---
> sympy/utilities/iterables.py | 25 +++++++++++++++++++++----
> sympy/utilities/tests/test_iterables.py | 6 ++++++
> 2 files changed, 27 insertions(+), 4 deletions(-)
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:
> 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
> + """
> + 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):
> + @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"""
> variations is a routine that calculates all possible variations
> of size n (with or without repetition) of a given set.
> ---
> doc/src/modules.txt | 1 +
> doc/src/modules/utilities/index.txt | 11 ++++++++
> doc/src/modules/utilities/iterables.txt | 39 +++++++++++++++++++++++++++++++
> sympy/utilities/iterables.py | 36 ++++++++++++++++++++++++++++
> sympy/utilities/tests/test_iterables.py | 6 ++++-
> 5 files changed, 92 insertions(+), 1 deletions(-)
> create mode 100644 doc/src/modules/utilities/index.txt
> create mode 100644 doc/src/modules/utilities/iterables.txt
> diff --git a/doc/src/modules.txt b/doc/src/modules.txt
> index fca4eb6..6c46683 100644
> --- a/doc/src/modules.txt
> +++ b/doc/src/modules.txt
> @@ -31,6 +31,7 @@ access any SymPy module, or use this contens:
> modules/statistics.txt
> modules/concrete.txt
> modules/solvers.txt
> + modules/utilities/index.txt
> Contributions to docs
> ---------------------
> diff --git a/doc/src/modules/utilities/index.txt b/doc/src/modules/utilities/index.txt
> new file mode 100644
> index 0000000..3e1e692
> --- /dev/null
> +++ b/doc/src/modules/utilities/index.txt
> @@ -0,0 +1,11 @@
> +Utilities
> +=========
> +
> +This module contains some general purpose utilities that are used across SymPy
> +
> +Contents:
> +
> +.. toctree::
> + :maxdepth: 2
> +
> + iterables.txt
> \ No newline at end of file
> diff --git a/doc/src/modules/utilities/iterables.txt b/doc/src/modules/utilities/iterables.txt
> new file mode 100644
> index 0000000..023a3bc
> --- /dev/null
> +++ b/doc/src/modules/utilities/iterables.txt
> @@ -0,0 +1,39 @@
> +Iterables
> +=========
> +
> +cartes
> +------
> +
> +Returns the cartesian product of two sequences
> +
> +Examples::
> + >>> from sympy.utilities.iterables import cartes
> + >>> cartes([1,2,3], ['a', 'b', 'c'])
> + [[1, 'a'],
> + [1, 'b'],
> + [1, 'c'],
> + [2, 'a'],
> + [2, 'b'],
> + [2, 'c'],
> + [3, 'a'],
> + [3, 'b'],
> + [3, 'c']]
> +
> +
> +
> +variations
> +----------
> +
> +variations(seq, n) Returns all the variations of the list of size n.
> +
> +Has an optional third argument. Must be a boolean value and makes the method
> +return the variations with repetition if set to True, or the variations
> +without repetition if set to False.
> +
> +Examples::
> + >>> from sympy.utilities.iterables import variations
> + >>> variations([1,2,3], 2)
> + [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
> + >>> variations([1,2,3], 2, True)
> + [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3], [3, 1], [3, 2], [3, 3]]
> +
> diff --git a/sympy/utilities/iterables.py b/sympy/utilities/iterables.py
> index 6179622..24ebcd0 100644
> --- a/sympy/utilities/iterables.py
> +++ b/sympy/utilities/iterables.py
> @@ -173,3 +173,39 @@ def recursion(result, M, k):
> for elem in recursion([item], M[i + 1:], k - 1):
> yield elem
> +
> +def cartes(seq0, seq1, modus='pair'):
> + """Return the cartesian product of two sequences """
> + if modus == 'pair':
> + return [[item0, item1] for item0 in seq0 for item1 in seq1]
> + elif modus == 'triple':
> + return [item0 + [item1] for item0 in seq0 for item1 in seq1]
> +
> +def variations(seq, n, repetition=False):
> + """Returns all the variations of the list of size n.
> +
> + variations(seq, n, True) will return all the variations of the list of
> + size n with repetitions
> +
> + variations(seq, n, False) will return all the variations of the list of
> + size n without repetitions
> + """
> + def setrep(seq): # remove sets with duplicates (repetition is relevant)
> + def delrep(seq): # remove duplicates while maintaining order
> + result = []
> + for item in seq:
> + if item not in result:
> + result.append(item)
> + return result
> + return [item for item in seq if item == delrep(item)]
> +
> + if n == 1:
> + return [[item] for item in seq]
> + result = range(len(seq))
> + cartesmodus = 'pair'
> + for i in range(n-1):
> + result = cartes(result, range(len(seq)), cartesmodus)
> + if not repetition:
> + result = setrep(result)
> + cartesmodus = 'triple'
> + return [[seq[index] for index in indices] for indices in result]
> diff --git a/sympy/utilities/tests/test_iterables.py b/sympy/utilities/tests/test_iterables.py
> index 1eea497..959c900 100644
> --- a/sympy/utilities/tests/test_iterables.py
> +++ b/sympy/utilities/tests/test_iterables.py
> @@ -1,6 +1,6 @@
> from sympy import symbols
> from sympy.utilities.iterables import postorder_traversal, \
> - preorder_traversal, flatten, subsets
> + preorder_traversal, flatten, subsets, variations
Also, how does this module compare to the assumptions logic module
that we already have in
sympy/core/facts.py
? Is it a duplicate functionality, or a new functionality, etc.? Is
it better/worse, what are your plans with facts.py (should we remove
it or maintain both)?
On Wed, Jun 10, 2009 at 11:41 AM, Ondrej Certik<ond...@certik.cz> wrote:
> 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
>> -
>> -
>> + # *******************
>> + # * 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")
>> +
> The old query system (.is_* properties) has some limitations:
> 1. Can't assume relations. It is not possible to specify relations between different > objects. For example, you can assume that x>0 and y>0, but you can't > assume x>y
> 2. Verbose code splitted across multiple classes. Each class must override > query methods (.is_*).
> 3. Extensibility. To add new assumptions you have to add code to > the core
> The assumption system implemented in this series of patches relies on these > principles:
> 1. Assumptions are separate from objects. Assumptions are instances of Assume, which is > a class that holds a reference to the expression and to the 'key' (what it assumes)
> 2. Queries out of the core. No more .is_* methods. Specific queries are implemented > as subclasses of QueryHandler, and the query function calls the appropriate > subclass of QueryHandler
> query -> QueryHandler -> QueryNegativeHandler > -> QueryIntegerHandler > -> QueryBoundedHandler > ... > That way, creating new queries is a matter of subclassing QueryHandler and override > the apropriate methods.
On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote:
> Before of this, sympy would convert True --> S.One, False --> S.Zero,
> but with the logic module with classes inheriting from basic, I need
> to manipulate boolean expressions, so converting booleans to S.One
> and S.Zero is in my oppinion wrong because of conceptual reasons
> and efficiency.
> The only module that used this behaviour was functions.elementary.piecewise,
> and was adopted to use the new one.
> ---
> sympy/core/sympify.py | 4 ++--
> sympy/functions/elementary/piecewise.py | 18 +++++++++++-------
> sympy/printing/ccode.py | 2 +-
> sympy/printing/latex.py | 2 +-
> sympy/printing/pretty/pretty.py | 2 +-
> sympy/utilities/decorator.py | 3 ++-
> 6 files changed, 18 insertions(+), 13 deletions(-)
> diff --git a/sympy/functions/elementary/piecewise.py b/sympy/functions/elementary/piecewise.py
> index d5afef7..3abbb1f 100644
> --- a/sympy/functions/elementary/piecewise.py
> +++ b/sympy/functions/elementary/piecewise.py
> @@ -147,7 +147,7 @@ def _eval_interval(self, sym, a, b):
> # - eg x < 1, x < 3 -> [oo,1],[1,3] instead of [oo,1],[oo,3]
> # 3) Sort the intervals to make it easier to find correct exprs
> for expr, cond in self.args:
> - if cond.is_Number:
> + if isinstance(cond, bool) or cond.is_Number:
> if cond:
> default = expr
> break
> @@ -203,15 +203,19 @@ def _eval_derivative(self, s):
> def _eval_subs(self, old, new):
> if self == old:
> return new
> - return Piecewise(*[(e._eval_subs(old, new), c._eval_subs(old, new)) \
> - for e, c in self.args])
> + new_args = []
> + for e, c in self.args:
> + if isinstance(c, bool):
> + new_args.append((e._eval_subs(old, new), c))
> + else:
> + new_args.append((e._eval_subs(old, new), c._eval_subs(old, new)))
> + return Piecewise( *new_args )
> @classmethod
> def __eval_cond(cls, cond):
> """Returns S.One if True, S.Zero if False, or None if undecidable."""
> - if type(cond) == bool or cond.is_number:
> - return sympify(bool(cond))
> - if cond.args[0].is_Number and cond.args[1].is_Number:
> - return sympify(bool(cond))
> + if type(cond) == bool or cond.is_number or (cond.args[0].is_Number and cond.args[1].is_Number):
> + if cond: return S.One
> + return S.Zero
> return None
> diff --git a/sympy/printing/ccode.py b/sympy/printing/ccode.py
> index a1341b5..7a6ccf6 100644
> --- a/sympy/printing/ccode.py
> +++ b/sympy/printing/ccode.py
> @@ -38,7 +38,7 @@ def _print_Piecewise(self, expr):
> ecpairs = ["(%s) {\n%s\n}\n" % (self._print(c), self._print(e)) \
> for e, c in expr.args[:-1]]
> last_line = ""
> - if expr.args[-1].cond is S.One:
> + if expr.args[-1].cond == True:
> last_line = "else {\n%s\n}" % self._print(expr.args[-1].expr)
> else:
> ecpairs.append("(%s) {\n%s\n" % \
> diff --git a/sympy/printing/latex.py b/sympy/printing/latex.py
> index 6f2b089..02cdd00 100644
> --- a/sympy/printing/latex.py
> +++ b/sympy/printing/latex.py
> @@ -521,7 +521,7 @@ def _print_Relational(self, expr):
> def _print_Piecewise(self, expr):
> ecpairs = [r"%s & for %s" % (self._print(e), self._print(c)) \
> for e, c in expr.args[:-1]]
> - if expr.args[-1].cond is S.One:
> + if expr.args[-1].cond == True:
> ecpairs.append(r"%s & \textrm{otherwise}" % \
> self._print(expr.args[-1].expr))
> else:
> diff --git a/sympy/printing/pretty/pretty.py b/sympy/printing/pretty/pretty.py
> index 5065d6c..19ffff2 100644
> --- a/sympy/printing/pretty/pretty.py
> +++ b/sympy/printing/pretty/pretty.py
> @@ -318,7 +318,7 @@ def _print_Piecewise(self, pexpr):
> P = {}
> for n, ec in enumerate(pexpr.args):
> P[n,0] = self._print(ec.expr)
> - if ec.cond is S.One:
> + if ec.cond == True:
> P[n,1] = prettyForm('otherwise')
> else:
> P[n,1] = prettyForm(*prettyForm('for ').right(self._print(ec.cond)))
> diff --git a/sympy/utilities/decorator.py b/sympy/utilities/decorator.py
> index 1d022d6..489f781 100644
> --- a/sympy/utilities/decorator.py
> +++ b/sympy/utilities/decorator.py
> @@ -39,6 +39,8 @@ def threaded_proxy(func):
> def threaded_decorator(expr, *args, **kwargs):
> if isinstance(expr, Matrix):
> return expr.applyfunc(lambda f: func(f, *args, **kwargs))
> + elif isinstance(expr, bool):
> + return expr
> elif hasattr(expr, '__iter__'):
> return expr.__class__([ func(f, *args, **kwargs) for f in expr ])
> else:
> @@ -47,7 +49,6 @@ def threaded_decorator(expr, *args, **kwargs):
> if isinstance(expr, Relational):
> lhs = func(expr.lhs, *args, **kwargs)
> rhs = func(expr.rhs, *args, **kwargs)
> -
> return Relational(lhs, rhs, expr.rel_op)
> elif expr.is_Add and use_add:
> return Add(*[ func(f, *args, **kwargs) for f in expr.args ])
> --
> 1.6.1.2
On Wed, Jun 10, 2009 at 10:45 AM, Fabian Pedregosa<fab...@fseoane.net> wrote:
> Fabian Pedregosa wrote: >> Ondrej Certik wrote: >>> On Wed, Jun 10, 2009 at 9:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote: >>>> The following are a series of patches that implement the new assumption system, which >>>> consists of:
>>>> - preparatory stuff (patches 1-5) >>>> - a logic module (much cleaner than the old one) >>>> - new style-assumptions (very simple objects) >>>> - query module, which substitutes the .is_* syntax
>>>> I tried hard to separate these 3 functionalities as much as possible.
>>>> This is more an initial implementation from whom we can star to work rather than a >>>> finished product, so in case you have any objections, we can start discussing.
>>>> Thanks for the review, >>> Could you also push this to some branch to github? I'll play with it.
> (notice branch is master, not assumptions) should be working now,
Patches are ok, tests pass. Code is ok too, I only had some minor remarks regarding docstrings and doctests. It's important to write nice docstrings, because they show up in our documentation. Those should not serve as tests though, only to guide the user, so that he has an idea what the function does.
Looks like very exciting work. I have a few questions about the new system...
One thing that really bothered me about the old .is_* syntax is that
it made it very difficult for users to create their own Sympy
subclasses and have them be true equals with the built-in Sympy types.
The reason is that for each new type you created, you would have to
modify the Sympy core to add the appropriate is_* syntax for your new
types.
It look like your patches will make it much easier for users to create
custom Sympy types that are on equal grounds as the built in ones. Is
this the case? If I create a new type, will I be able to implement
queries for that type and will I be able to tell the Sympy core about
my objects without modifying the Sympy core?
I think we should make it a goal that new sympy types can be created
without modifying Sympy core. Another area that currently violates
this idea is the printing system. But, this will have to be modified
anyways to get it to work with the Sage notebook's handling of latex
output.
On Wed, Jun 10, 2009 at 8:20 AM, Fabian Pedregosa<fab...@fseoane.net> wrote:
> The following are a series of patches that implement the new assumption system, which
> consists of:
> - preparatory stuff (patches 1-5)
> - a logic module (much cleaner than the old one)
> - new style-assumptions (very simple objects)
> - query module, which substitutes the .is_* syntax
> I tried hard to separate these 3 functionalities as much as possible.
> This is more an initial implementation from whom we can star to work rather than a
> finished product, so in case you have any objections, we can start discussing.
> Thanks for the review,
> - Fabian
> pd: patch 3 was already approved, just that i did not push it in.
On Wed, Jun 10, 2009 at 12:12 PM, Brian Granger<ellisonbg....@gmail.com> wrote:
> Fabian,
> Looks like very exciting work. I have a few questions about the new system...
> One thing that really bothered me about the old .is_* syntax is that > it made it very difficult for users to create their own Sympy > subclasses and have them be true equals with the built-in Sympy types. > The reason is that for each new type you created, you would have to > modify the Sympy core to add the appropriate is_* syntax for your new > types.
> It look like your patches will make it much easier for users to create > custom Sympy types that are on equal grounds as the built in ones. Is > this the case? If I create a new type, will I be able to implement > queries for that type and will I be able to tell the Sympy core about > my objects without modifying the Sympy core?
Yes, definitely!
> I think we should make it a goal that new sympy types can be created > without modifying Sympy core. Another area that currently violates > this idea is the printing system. But, this will have to be modified > anyways to get it to work with the Sage notebook's handling of latex > output.
It has to be fixed for the sage notebook, but otherwise how does it violate it?
If you create your own class and want to hook it up let's say in the latex printer, just override the _latex_ method.
The same way, if you want to create your own printer, just override or create a new Printer class from scratch. That way, sympy can be extented both by new classes and new printers. What is your idea to do it some other way?