Coverage for /usr/lib/python3/dist-packages/sympy/core/logic.py: 33%
199 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Logic expressions handling
3NOTE
4----
6at present this is mainly needed for facts.py, feel free however to improve
7this stuff for general purpose.
8"""
10from __future__ import annotations
11from typing import Optional
13# Type of a fuzzy bool
14FuzzyBool = Optional[bool]
17def _torf(args):
18 """Return True if all args are True, False if they
19 are all False, else None.
21 >>> from sympy.core.logic import _torf
22 >>> _torf((True, True))
23 True
24 >>> _torf((False, False))
25 False
26 >>> _torf((True, False))
27 """
28 sawT = sawF = False
29 for a in args:
30 if a is True:
31 if sawF:
32 return
33 sawT = True
34 elif a is False:
35 if sawT:
36 return
37 sawF = True
38 else:
39 return
40 return sawT
43def _fuzzy_group(args, quick_exit=False):
44 """Return True if all args are True, None if there is any None else False
45 unless ``quick_exit`` is True (then return None as soon as a second False
46 is seen.
48 ``_fuzzy_group`` is like ``fuzzy_and`` except that it is more
49 conservative in returning a False, waiting to make sure that all
50 arguments are True or False and returning None if any arguments are
51 None. It also has the capability of permiting only a single False and
52 returning None if more than one is seen. For example, the presence of a
53 single transcendental amongst rationals would indicate that the group is
54 no longer rational; but a second transcendental in the group would make the
55 determination impossible.
58 Examples
59 ========
61 >>> from sympy.core.logic import _fuzzy_group
63 By default, multiple Falses mean the group is broken:
65 >>> _fuzzy_group([False, False, True])
66 False
68 If multiple Falses mean the group status is unknown then set
69 `quick_exit` to True so None can be returned when the 2nd False is seen:
71 >>> _fuzzy_group([False, False, True], quick_exit=True)
73 But if only a single False is seen then the group is known to
74 be broken:
76 >>> _fuzzy_group([False, True, True], quick_exit=True)
77 False
79 """
80 saw_other = False
81 for a in args:
82 if a is True:
83 continue
84 if a is None:
85 return
86 if quick_exit and saw_other:
87 return
88 saw_other = True
89 return not saw_other
92def fuzzy_bool(x):
93 """Return True, False or None according to x.
95 Whereas bool(x) returns True or False, fuzzy_bool allows
96 for the None value and non-false values (which become None), too.
98 Examples
99 ========
101 >>> from sympy.core.logic import fuzzy_bool
102 >>> from sympy.abc import x
103 >>> fuzzy_bool(x), fuzzy_bool(None)
104 (None, None)
105 >>> bool(x), bool(None)
106 (True, False)
108 """
109 if x is None:
110 return None
111 if x in (True, False):
112 return bool(x)
115def fuzzy_and(args):
116 """Return True (all True), False (any False) or None.
118 Examples
119 ========
121 >>> from sympy.core.logic import fuzzy_and
122 >>> from sympy import Dummy
124 If you had a list of objects to test the commutivity of
125 and you want the fuzzy_and logic applied, passing an
126 iterator will allow the commutativity to only be computed
127 as many times as necessary. With this list, False can be
128 returned after analyzing the first symbol:
130 >>> syms = [Dummy(commutative=False), Dummy()]
131 >>> fuzzy_and(s.is_commutative for s in syms)
132 False
134 That False would require less work than if a list of pre-computed
135 items was sent:
137 >>> fuzzy_and([s.is_commutative for s in syms])
138 False
139 """
141 rv = True
142 for ai in args:
143 ai = fuzzy_bool(ai)
144 if ai is False:
145 return False
146 if rv: # this will stop updating if a None is ever trapped
147 rv = ai
148 return rv
151def fuzzy_not(v):
152 """
153 Not in fuzzy logic
155 Return None if `v` is None else `not v`.
157 Examples
158 ========
160 >>> from sympy.core.logic import fuzzy_not
161 >>> fuzzy_not(True)
162 False
163 >>> fuzzy_not(None)
164 >>> fuzzy_not(False)
165 True
167 """
168 if v is None:
169 return v
170 else:
171 return not v
174def fuzzy_or(args):
175 """
176 Or in fuzzy logic. Returns True (any True), False (all False), or None
178 See the docstrings of fuzzy_and and fuzzy_not for more info. fuzzy_or is
179 related to the two by the standard De Morgan's law.
181 >>> from sympy.core.logic import fuzzy_or
182 >>> fuzzy_or([True, False])
183 True
184 >>> fuzzy_or([True, None])
185 True
186 >>> fuzzy_or([False, False])
187 False
188 >>> print(fuzzy_or([False, None]))
189 None
191 """
192 rv = False
193 for ai in args:
194 ai = fuzzy_bool(ai)
195 if ai is True:
196 return True
197 if rv is False: # this will stop updating if a None is ever trapped
198 rv = ai
199 return rv
202def fuzzy_xor(args):
203 """Return None if any element of args is not True or False, else
204 True (if there are an odd number of True elements), else False."""
205 t = f = 0
206 for a in args:
207 ai = fuzzy_bool(a)
208 if ai:
209 t += 1
210 elif ai is False:
211 f += 1
212 else:
213 return
214 return t % 2 == 1
217def fuzzy_nand(args):
218 """Return False if all args are True, True if they are all False,
219 else None."""
220 return fuzzy_not(fuzzy_and(args))
223class Logic:
224 """Logical expression"""
225 # {} 'op' -> LogicClass
226 op_2class: dict[str, type[Logic]] = {}
228 def __new__(cls, *args):
229 obj = object.__new__(cls)
230 obj.args = args
231 return obj
233 def __getnewargs__(self):
234 return self.args
236 def __hash__(self):
237 return hash((type(self).__name__,) + tuple(self.args))
239 def __eq__(a, b):
240 if not isinstance(b, type(a)):
241 return False
242 else:
243 return a.args == b.args
245 def __ne__(a, b):
246 if not isinstance(b, type(a)):
247 return True
248 else:
249 return a.args != b.args
251 def __lt__(self, other):
252 if self.__cmp__(other) == -1:
253 return True
254 return False
256 def __cmp__(self, other):
257 if type(self) is not type(other):
258 a = str(type(self))
259 b = str(type(other))
260 else:
261 a = self.args
262 b = other.args
263 return (a > b) - (a < b)
265 def __str__(self):
266 return '%s(%s)' % (self.__class__.__name__,
267 ', '.join(str(a) for a in self.args))
269 __repr__ = __str__
271 @staticmethod
272 def fromstring(text):
273 """Logic from string with space around & and | but none after !.
275 e.g.
277 !a & b | c
278 """
279 lexpr = None # current logical expression
280 schedop = None # scheduled operation
281 for term in text.split():
282 # operation symbol
283 if term in '&|':
284 if schedop is not None:
285 raise ValueError(
286 'double op forbidden: "%s %s"' % (term, schedop))
287 if lexpr is None:
288 raise ValueError(
289 '%s cannot be in the beginning of expression' % term)
290 schedop = term
291 continue
292 if '&' in term or '|' in term:
293 raise ValueError('& and | must have space around them')
294 if term[0] == '!':
295 if len(term) == 1:
296 raise ValueError('do not include space after "!"')
297 term = Not(term[1:])
299 # already scheduled operation, e.g. '&'
300 if schedop:
301 lexpr = Logic.op_2class[schedop](lexpr, term)
302 schedop = None
303 continue
305 # this should be atom
306 if lexpr is not None:
307 raise ValueError(
308 'missing op between "%s" and "%s"' % (lexpr, term))
310 lexpr = term
312 # let's check that we ended up in correct state
313 if schedop is not None:
314 raise ValueError('premature end-of-expression in "%s"' % text)
315 if lexpr is None:
316 raise ValueError('"%s" is empty' % text)
318 # everything looks good now
319 return lexpr
322class AndOr_Base(Logic):
324 def __new__(cls, *args):
325 bargs = []
326 for a in args:
327 if a == cls.op_x_notx:
328 return a
329 elif a == (not cls.op_x_notx):
330 continue # skip this argument
331 bargs.append(a)
333 args = sorted(set(cls.flatten(bargs)), key=hash)
335 for a in args:
336 if Not(a) in args:
337 return cls.op_x_notx
339 if len(args) == 1:
340 return args.pop()
341 elif len(args) == 0:
342 return not cls.op_x_notx
344 return Logic.__new__(cls, *args)
346 @classmethod
347 def flatten(cls, args):
348 # quick-n-dirty flattening for And and Or
349 args_queue = list(args)
350 res = []
352 while True:
353 try:
354 arg = args_queue.pop(0)
355 except IndexError:
356 break
357 if isinstance(arg, Logic):
358 if isinstance(arg, cls):
359 args_queue.extend(arg.args)
360 continue
361 res.append(arg)
363 args = tuple(res)
364 return args
367class And(AndOr_Base):
368 op_x_notx = False
370 def _eval_propagate_not(self):
371 # !(a&b&c ...) == !a | !b | !c ...
372 return Or(*[Not(a) for a in self.args])
374 # (a|b|...) & c == (a&c) | (b&c) | ...
375 def expand(self):
377 # first locate Or
378 for i, arg in enumerate(self.args):
379 if isinstance(arg, Or):
380 arest = self.args[:i] + self.args[i + 1:]
382 orterms = [And(*(arest + (a,))) for a in arg.args]
383 for j in range(len(orterms)):
384 if isinstance(orterms[j], Logic):
385 orterms[j] = orterms[j].expand()
387 res = Or(*orterms)
388 return res
390 return self
393class Or(AndOr_Base):
394 op_x_notx = True
396 def _eval_propagate_not(self):
397 # !(a|b|c ...) == !a & !b & !c ...
398 return And(*[Not(a) for a in self.args])
401class Not(Logic):
403 def __new__(cls, arg):
404 if isinstance(arg, str):
405 return Logic.__new__(cls, arg)
407 elif isinstance(arg, bool):
408 return not arg
409 elif isinstance(arg, Not):
410 return arg.args[0]
412 elif isinstance(arg, Logic):
413 # XXX this is a hack to expand right from the beginning
414 arg = arg._eval_propagate_not()
415 return arg
417 else:
418 raise ValueError('Not: unknown argument %r' % (arg,))
420 @property
421 def arg(self):
422 return self.args[0]
425Logic.op_2class['&'] = And
426Logic.op_2class['|'] = Or
427Logic.op_2class['!'] = Not