logic.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. """Logic expressions handling
  2. NOTE
  3. ----
  4. at present this is mainly needed for facts.py, feel free however to improve
  5. this stuff for general purpose.
  6. """
  7. from __future__ import annotations
  8. from typing import Optional
  9. # Type of a fuzzy bool
  10. FuzzyBool = Optional[bool]
  11. def _torf(args):
  12. """Return True if all args are True, False if they
  13. are all False, else None.
  14. >>> from sympy.core.logic import _torf
  15. >>> _torf((True, True))
  16. True
  17. >>> _torf((False, False))
  18. False
  19. >>> _torf((True, False))
  20. """
  21. sawT = sawF = False
  22. for a in args:
  23. if a is True:
  24. if sawF:
  25. return
  26. sawT = True
  27. elif a is False:
  28. if sawT:
  29. return
  30. sawF = True
  31. else:
  32. return
  33. return sawT
  34. def _fuzzy_group(args, quick_exit=False):
  35. """Return True if all args are True, None if there is any None else False
  36. unless ``quick_exit`` is True (then return None as soon as a second False
  37. is seen.
  38. ``_fuzzy_group`` is like ``fuzzy_and`` except that it is more
  39. conservative in returning a False, waiting to make sure that all
  40. arguments are True or False and returning None if any arguments are
  41. None. It also has the capability of permiting only a single False and
  42. returning None if more than one is seen. For example, the presence of a
  43. single transcendental amongst rationals would indicate that the group is
  44. no longer rational; but a second transcendental in the group would make the
  45. determination impossible.
  46. Examples
  47. ========
  48. >>> from sympy.core.logic import _fuzzy_group
  49. By default, multiple Falses mean the group is broken:
  50. >>> _fuzzy_group([False, False, True])
  51. False
  52. If multiple Falses mean the group status is unknown then set
  53. `quick_exit` to True so None can be returned when the 2nd False is seen:
  54. >>> _fuzzy_group([False, False, True], quick_exit=True)
  55. But if only a single False is seen then the group is known to
  56. be broken:
  57. >>> _fuzzy_group([False, True, True], quick_exit=True)
  58. False
  59. """
  60. saw_other = False
  61. for a in args:
  62. if a is True:
  63. continue
  64. if a is None:
  65. return
  66. if quick_exit and saw_other:
  67. return
  68. saw_other = True
  69. return not saw_other
  70. def fuzzy_bool(x):
  71. """Return True, False or None according to x.
  72. Whereas bool(x) returns True or False, fuzzy_bool allows
  73. for the None value and non-false values (which become None), too.
  74. Examples
  75. ========
  76. >>> from sympy.core.logic import fuzzy_bool
  77. >>> from sympy.abc import x
  78. >>> fuzzy_bool(x), fuzzy_bool(None)
  79. (None, None)
  80. >>> bool(x), bool(None)
  81. (True, False)
  82. """
  83. if x is None:
  84. return None
  85. if x in (True, False):
  86. return bool(x)
  87. def fuzzy_and(args):
  88. """Return True (all True), False (any False) or None.
  89. Examples
  90. ========
  91. >>> from sympy.core.logic import fuzzy_and
  92. >>> from sympy import Dummy
  93. If you had a list of objects to test the commutivity of
  94. and you want the fuzzy_and logic applied, passing an
  95. iterator will allow the commutativity to only be computed
  96. as many times as necessary. With this list, False can be
  97. returned after analyzing the first symbol:
  98. >>> syms = [Dummy(commutative=False), Dummy()]
  99. >>> fuzzy_and(s.is_commutative for s in syms)
  100. False
  101. That False would require less work than if a list of pre-computed
  102. items was sent:
  103. >>> fuzzy_and([s.is_commutative for s in syms])
  104. False
  105. """
  106. rv = True
  107. for ai in args:
  108. ai = fuzzy_bool(ai)
  109. if ai is False:
  110. return False
  111. if rv: # this will stop updating if a None is ever trapped
  112. rv = ai
  113. return rv
  114. def fuzzy_not(v):
  115. """
  116. Not in fuzzy logic
  117. Return None if `v` is None else `not v`.
  118. Examples
  119. ========
  120. >>> from sympy.core.logic import fuzzy_not
  121. >>> fuzzy_not(True)
  122. False
  123. >>> fuzzy_not(None)
  124. >>> fuzzy_not(False)
  125. True
  126. """
  127. if v is None:
  128. return v
  129. else:
  130. return not v
  131. def fuzzy_or(args):
  132. """
  133. Or in fuzzy logic. Returns True (any True), False (all False), or None
  134. See the docstrings of fuzzy_and and fuzzy_not for more info. fuzzy_or is
  135. related to the two by the standard De Morgan's law.
  136. >>> from sympy.core.logic import fuzzy_or
  137. >>> fuzzy_or([True, False])
  138. True
  139. >>> fuzzy_or([True, None])
  140. True
  141. >>> fuzzy_or([False, False])
  142. False
  143. >>> print(fuzzy_or([False, None]))
  144. None
  145. """
  146. rv = False
  147. for ai in args:
  148. ai = fuzzy_bool(ai)
  149. if ai is True:
  150. return True
  151. if rv is False: # this will stop updating if a None is ever trapped
  152. rv = ai
  153. return rv
  154. def fuzzy_xor(args):
  155. """Return None if any element of args is not True or False, else
  156. True (if there are an odd number of True elements), else False."""
  157. t = f = 0
  158. for a in args:
  159. ai = fuzzy_bool(a)
  160. if ai:
  161. t += 1
  162. elif ai is False:
  163. f += 1
  164. else:
  165. return
  166. return t % 2 == 1
  167. def fuzzy_nand(args):
  168. """Return False if all args are True, True if they are all False,
  169. else None."""
  170. return fuzzy_not(fuzzy_and(args))
  171. class Logic:
  172. """Logical expression"""
  173. # {} 'op' -> LogicClass
  174. op_2class: dict[str, type[Logic]] = {}
  175. def __new__(cls, *args):
  176. obj = object.__new__(cls)
  177. obj.args = args
  178. return obj
  179. def __getnewargs__(self):
  180. return self.args
  181. def __hash__(self):
  182. return hash((type(self).__name__,) + tuple(self.args))
  183. def __eq__(a, b):
  184. if not isinstance(b, type(a)):
  185. return False
  186. else:
  187. return a.args == b.args
  188. def __ne__(a, b):
  189. if not isinstance(b, type(a)):
  190. return True
  191. else:
  192. return a.args != b.args
  193. def __lt__(self, other):
  194. if self.__cmp__(other) == -1:
  195. return True
  196. return False
  197. def __cmp__(self, other):
  198. if type(self) is not type(other):
  199. a = str(type(self))
  200. b = str(type(other))
  201. else:
  202. a = self.args
  203. b = other.args
  204. return (a > b) - (a < b)
  205. def __str__(self):
  206. return '%s(%s)' % (self.__class__.__name__,
  207. ', '.join(str(a) for a in self.args))
  208. __repr__ = __str__
  209. @staticmethod
  210. def fromstring(text):
  211. """Logic from string with space around & and | but none after !.
  212. e.g.
  213. !a & b | c
  214. """
  215. lexpr = None # current logical expression
  216. schedop = None # scheduled operation
  217. for term in text.split():
  218. # operation symbol
  219. if term in '&|':
  220. if schedop is not None:
  221. raise ValueError(
  222. 'double op forbidden: "%s %s"' % (term, schedop))
  223. if lexpr is None:
  224. raise ValueError(
  225. '%s cannot be in the beginning of expression' % term)
  226. schedop = term
  227. continue
  228. if '&' in term or '|' in term:
  229. raise ValueError('& and | must have space around them')
  230. if term[0] == '!':
  231. if len(term) == 1:
  232. raise ValueError('do not include space after "!"')
  233. term = Not(term[1:])
  234. # already scheduled operation, e.g. '&'
  235. if schedop:
  236. lexpr = Logic.op_2class[schedop](lexpr, term)
  237. schedop = None
  238. continue
  239. # this should be atom
  240. if lexpr is not None:
  241. raise ValueError(
  242. 'missing op between "%s" and "%s"' % (lexpr, term))
  243. lexpr = term
  244. # let's check that we ended up in correct state
  245. if schedop is not None:
  246. raise ValueError('premature end-of-expression in "%s"' % text)
  247. if lexpr is None:
  248. raise ValueError('"%s" is empty' % text)
  249. # everything looks good now
  250. return lexpr
  251. class AndOr_Base(Logic):
  252. def __new__(cls, *args):
  253. bargs = []
  254. for a in args:
  255. if a == cls.op_x_notx:
  256. return a
  257. elif a == (not cls.op_x_notx):
  258. continue # skip this argument
  259. bargs.append(a)
  260. args = sorted(set(cls.flatten(bargs)), key=hash)
  261. for a in args:
  262. if Not(a) in args:
  263. return cls.op_x_notx
  264. if len(args) == 1:
  265. return args.pop()
  266. elif len(args) == 0:
  267. return not cls.op_x_notx
  268. return Logic.__new__(cls, *args)
  269. @classmethod
  270. def flatten(cls, args):
  271. # quick-n-dirty flattening for And and Or
  272. args_queue = list(args)
  273. res = []
  274. while True:
  275. try:
  276. arg = args_queue.pop(0)
  277. except IndexError:
  278. break
  279. if isinstance(arg, Logic):
  280. if isinstance(arg, cls):
  281. args_queue.extend(arg.args)
  282. continue
  283. res.append(arg)
  284. args = tuple(res)
  285. return args
  286. class And(AndOr_Base):
  287. op_x_notx = False
  288. def _eval_propagate_not(self):
  289. # !(a&b&c ...) == !a | !b | !c ...
  290. return Or(*[Not(a) for a in self.args])
  291. # (a|b|...) & c == (a&c) | (b&c) | ...
  292. def expand(self):
  293. # first locate Or
  294. for i, arg in enumerate(self.args):
  295. if isinstance(arg, Or):
  296. arest = self.args[:i] + self.args[i + 1:]
  297. orterms = [And(*(arest + (a,))) for a in arg.args]
  298. for j in range(len(orterms)):
  299. if isinstance(orterms[j], Logic):
  300. orterms[j] = orterms[j].expand()
  301. res = Or(*orterms)
  302. return res
  303. return self
  304. class Or(AndOr_Base):
  305. op_x_notx = True
  306. def _eval_propagate_not(self):
  307. # !(a|b|c ...) == !a & !b & !c ...
  308. return And(*[Not(a) for a in self.args])
  309. class Not(Logic):
  310. def __new__(cls, arg):
  311. if isinstance(arg, str):
  312. return Logic.__new__(cls, arg)
  313. elif isinstance(arg, bool):
  314. return not arg
  315. elif isinstance(arg, Not):
  316. return arg.args[0]
  317. elif isinstance(arg, Logic):
  318. # XXX this is a hack to expand right from the beginning
  319. arg = arg._eval_propagate_not()
  320. return arg
  321. else:
  322. raise ValueError('Not: unknown argument %r' % (arg,))
  323. @property
  324. def arg(self):
  325. return self.args[0]
  326. Logic.op_2class['&'] = And
  327. Logic.op_2class['|'] = Or
  328. Logic.op_2class['!'] = Not