|
"""Logic expressions handling |
|
|
|
NOTE |
|
---- |
|
|
|
at present this is mainly needed for facts.py, feel free however to improve |
|
this stuff for general purpose. |
|
""" |
|
|
|
from __future__ import annotations |
|
from typing import Optional |
|
|
|
|
|
FuzzyBool = Optional[bool] |
|
|
|
|
|
def _torf(args): |
|
"""Return True if all args are True, False if they |
|
are all False, else None. |
|
|
|
>>> from sympy.core.logic import _torf |
|
>>> _torf((True, True)) |
|
True |
|
>>> _torf((False, False)) |
|
False |
|
>>> _torf((True, False)) |
|
""" |
|
sawT = sawF = False |
|
for a in args: |
|
if a is True: |
|
if sawF: |
|
return |
|
sawT = True |
|
elif a is False: |
|
if sawT: |
|
return |
|
sawF = True |
|
else: |
|
return |
|
return sawT |
|
|
|
|
|
def _fuzzy_group(args, quick_exit=False): |
|
"""Return True if all args are True, None if there is any None else False |
|
unless ``quick_exit`` is True (then return None as soon as a second False |
|
is seen. |
|
|
|
``_fuzzy_group`` is like ``fuzzy_and`` except that it is more |
|
conservative in returning a False, waiting to make sure that all |
|
arguments are True or False and returning None if any arguments are |
|
None. It also has the capability of permiting only a single False and |
|
returning None if more than one is seen. For example, the presence of a |
|
single transcendental amongst rationals would indicate that the group is |
|
no longer rational; but a second transcendental in the group would make the |
|
determination impossible. |
|
|
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.core.logic import _fuzzy_group |
|
|
|
By default, multiple Falses mean the group is broken: |
|
|
|
>>> _fuzzy_group([False, False, True]) |
|
False |
|
|
|
If multiple Falses mean the group status is unknown then set |
|
`quick_exit` to True so None can be returned when the 2nd False is seen: |
|
|
|
>>> _fuzzy_group([False, False, True], quick_exit=True) |
|
|
|
But if only a single False is seen then the group is known to |
|
be broken: |
|
|
|
>>> _fuzzy_group([False, True, True], quick_exit=True) |
|
False |
|
|
|
""" |
|
saw_other = False |
|
for a in args: |
|
if a is True: |
|
continue |
|
if a is None: |
|
return |
|
if quick_exit and saw_other: |
|
return |
|
saw_other = True |
|
return not saw_other |
|
|
|
|
|
def fuzzy_bool(x): |
|
"""Return True, False or None according to x. |
|
|
|
Whereas bool(x) returns True or False, fuzzy_bool allows |
|
for the None value and non-false values (which become None), too. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.core.logic import fuzzy_bool |
|
>>> from sympy.abc import x |
|
>>> fuzzy_bool(x), fuzzy_bool(None) |
|
(None, None) |
|
>>> bool(x), bool(None) |
|
(True, False) |
|
|
|
""" |
|
if x is None: |
|
return None |
|
if x in (True, False): |
|
return bool(x) |
|
|
|
|
|
def fuzzy_and(args): |
|
"""Return True (all True), False (any False) or None. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.core.logic import fuzzy_and |
|
>>> from sympy import Dummy |
|
|
|
If you had a list of objects to test the commutivity of |
|
and you want the fuzzy_and logic applied, passing an |
|
iterator will allow the commutativity to only be computed |
|
as many times as necessary. With this list, False can be |
|
returned after analyzing the first symbol: |
|
|
|
>>> syms = [Dummy(commutative=False), Dummy()] |
|
>>> fuzzy_and(s.is_commutative for s in syms) |
|
False |
|
|
|
That False would require less work than if a list of pre-computed |
|
items was sent: |
|
|
|
>>> fuzzy_and([s.is_commutative for s in syms]) |
|
False |
|
""" |
|
|
|
rv = True |
|
for ai in args: |
|
ai = fuzzy_bool(ai) |
|
if ai is False: |
|
return False |
|
if rv: |
|
rv = ai |
|
return rv |
|
|
|
|
|
def fuzzy_not(v): |
|
""" |
|
Not in fuzzy logic |
|
|
|
Return None if `v` is None else `not v`. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.core.logic import fuzzy_not |
|
>>> fuzzy_not(True) |
|
False |
|
>>> fuzzy_not(None) |
|
>>> fuzzy_not(False) |
|
True |
|
|
|
""" |
|
if v is None: |
|
return v |
|
else: |
|
return not v |
|
|
|
|
|
def fuzzy_or(args): |
|
""" |
|
Or in fuzzy logic. Returns True (any True), False (all False), or None |
|
|
|
See the docstrings of fuzzy_and and fuzzy_not for more info. fuzzy_or is |
|
related to the two by the standard De Morgan's law. |
|
|
|
>>> from sympy.core.logic import fuzzy_or |
|
>>> fuzzy_or([True, False]) |
|
True |
|
>>> fuzzy_or([True, None]) |
|
True |
|
>>> fuzzy_or([False, False]) |
|
False |
|
>>> print(fuzzy_or([False, None])) |
|
None |
|
|
|
""" |
|
rv = False |
|
for ai in args: |
|
ai = fuzzy_bool(ai) |
|
if ai is True: |
|
return True |
|
if rv is False: |
|
rv = ai |
|
return rv |
|
|
|
|
|
def fuzzy_xor(args): |
|
"""Return None if any element of args is not True or False, else |
|
True (if there are an odd number of True elements), else False.""" |
|
t = f = 0 |
|
for a in args: |
|
ai = fuzzy_bool(a) |
|
if ai: |
|
t += 1 |
|
elif ai is False: |
|
f += 1 |
|
else: |
|
return |
|
return t % 2 == 1 |
|
|
|
|
|
def fuzzy_nand(args): |
|
"""Return False if all args are True, True if they are all False, |
|
else None.""" |
|
return fuzzy_not(fuzzy_and(args)) |
|
|
|
|
|
class Logic: |
|
"""Logical expression""" |
|
|
|
op_2class: dict[str, type[Logic]] = {} |
|
|
|
def __new__(cls, *args): |
|
obj = object.__new__(cls) |
|
obj.args = args |
|
return obj |
|
|
|
def __getnewargs__(self): |
|
return self.args |
|
|
|
def __hash__(self): |
|
return hash((type(self).__name__,) + tuple(self.args)) |
|
|
|
def __eq__(a, b): |
|
if not isinstance(b, type(a)): |
|
return False |
|
else: |
|
return a.args == b.args |
|
|
|
def __ne__(a, b): |
|
if not isinstance(b, type(a)): |
|
return True |
|
else: |
|
return a.args != b.args |
|
|
|
def __lt__(self, other): |
|
if self.__cmp__(other) == -1: |
|
return True |
|
return False |
|
|
|
def __cmp__(self, other): |
|
if type(self) is not type(other): |
|
a = str(type(self)) |
|
b = str(type(other)) |
|
else: |
|
a = self.args |
|
b = other.args |
|
return (a > b) - (a < b) |
|
|
|
def __str__(self): |
|
return '%s(%s)' % (self.__class__.__name__, |
|
', '.join(str(a) for a in self.args)) |
|
|
|
__repr__ = __str__ |
|
|
|
@staticmethod |
|
def fromstring(text): |
|
"""Logic from string with space around & and | but none after !. |
|
|
|
e.g. |
|
|
|
!a & b | c |
|
""" |
|
lexpr = None |
|
schedop = None |
|
for term in text.split(): |
|
|
|
if term in '&|': |
|
if schedop is not None: |
|
raise ValueError( |
|
'double op forbidden: "%s %s"' % (term, schedop)) |
|
if lexpr is None: |
|
raise ValueError( |
|
'%s cannot be in the beginning of expression' % term) |
|
schedop = term |
|
continue |
|
if '&' in term or '|' in term: |
|
raise ValueError('& and | must have space around them') |
|
if term[0] == '!': |
|
if len(term) == 1: |
|
raise ValueError('do not include space after "!"') |
|
term = Not(term[1:]) |
|
|
|
|
|
if schedop: |
|
lexpr = Logic.op_2class[schedop](lexpr, term) |
|
schedop = None |
|
continue |
|
|
|
|
|
if lexpr is not None: |
|
raise ValueError( |
|
'missing op between "%s" and "%s"' % (lexpr, term)) |
|
|
|
lexpr = term |
|
|
|
|
|
if schedop is not None: |
|
raise ValueError('premature end-of-expression in "%s"' % text) |
|
if lexpr is None: |
|
raise ValueError('"%s" is empty' % text) |
|
|
|
|
|
return lexpr |
|
|
|
|
|
class AndOr_Base(Logic): |
|
|
|
def __new__(cls, *args): |
|
bargs = [] |
|
for a in args: |
|
if a == cls.op_x_notx: |
|
return a |
|
elif a == (not cls.op_x_notx): |
|
continue |
|
bargs.append(a) |
|
|
|
args = sorted(set(cls.flatten(bargs)), key=hash) |
|
|
|
for a in args: |
|
if Not(a) in args: |
|
return cls.op_x_notx |
|
|
|
if len(args) == 1: |
|
return args.pop() |
|
elif len(args) == 0: |
|
return not cls.op_x_notx |
|
|
|
return Logic.__new__(cls, *args) |
|
|
|
@classmethod |
|
def flatten(cls, args): |
|
|
|
args_queue = list(args) |
|
res = [] |
|
|
|
while True: |
|
try: |
|
arg = args_queue.pop(0) |
|
except IndexError: |
|
break |
|
if isinstance(arg, Logic): |
|
if isinstance(arg, cls): |
|
args_queue.extend(arg.args) |
|
continue |
|
res.append(arg) |
|
|
|
args = tuple(res) |
|
return args |
|
|
|
|
|
class And(AndOr_Base): |
|
op_x_notx = False |
|
|
|
def _eval_propagate_not(self): |
|
|
|
return Or(*[Not(a) for a in self.args]) |
|
|
|
|
|
def expand(self): |
|
|
|
|
|
for i, arg in enumerate(self.args): |
|
if isinstance(arg, Or): |
|
arest = self.args[:i] + self.args[i + 1:] |
|
|
|
orterms = [And(*(arest + (a,))) for a in arg.args] |
|
for j in range(len(orterms)): |
|
if isinstance(orterms[j], Logic): |
|
orterms[j] = orterms[j].expand() |
|
|
|
res = Or(*orterms) |
|
return res |
|
|
|
return self |
|
|
|
|
|
class Or(AndOr_Base): |
|
op_x_notx = True |
|
|
|
def _eval_propagate_not(self): |
|
|
|
return And(*[Not(a) for a in self.args]) |
|
|
|
|
|
class Not(Logic): |
|
|
|
def __new__(cls, arg): |
|
if isinstance(arg, str): |
|
return Logic.__new__(cls, arg) |
|
|
|
elif isinstance(arg, bool): |
|
return not arg |
|
elif isinstance(arg, Not): |
|
return arg.args[0] |
|
|
|
elif isinstance(arg, Logic): |
|
|
|
arg = arg._eval_propagate_not() |
|
return arg |
|
|
|
else: |
|
raise ValueError('Not: unknown argument %r' % (arg,)) |
|
|
|
@property |
|
def arg(self): |
|
return self.args[0] |
|
|
|
|
|
Logic.op_2class['&'] = And |
|
Logic.op_2class['|'] = Or |
|
Logic.op_2class['!'] = Not |
|
|