123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- """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
- # Type of a fuzzy bool
- 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: # this will stop updating if a None is ever trapped
- 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: # this will stop updating if a None is ever trapped
- 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' -> LogicClass
- 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 # current logical expression
- schedop = None # scheduled operation
- for term in text.split():
- # operation symbol
- 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:])
- # already scheduled operation, e.g. '&'
- if schedop:
- lexpr = Logic.op_2class[schedop](lexpr, term)
- schedop = None
- continue
- # this should be atom
- if lexpr is not None:
- raise ValueError(
- 'missing op between "%s" and "%s"' % (lexpr, term))
- lexpr = term
- # let's check that we ended up in correct state
- if schedop is not None:
- raise ValueError('premature end-of-expression in "%s"' % text)
- if lexpr is None:
- raise ValueError('"%s" is empty' % text)
- # everything looks good now
- 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 # skip this argument
- 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):
- # quick-n-dirty flattening for And and Or
- 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):
- # !(a&b&c ...) == !a | !b | !c ...
- return Or(*[Not(a) for a in self.args])
- # (a|b|...) & c == (a&c) | (b&c) | ...
- def expand(self):
- # first locate Or
- 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):
- # !(a|b|c ...) == !a & !b & !c ...
- 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):
- # XXX this is a hack to expand right from the beginning
- 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
|