|
""" |
|
Boolean algebra module for SymPy |
|
""" |
|
|
|
from __future__ import annotations |
|
from typing import TYPE_CHECKING, overload, Any |
|
from collections.abc import Iterable, Mapping |
|
|
|
from collections import defaultdict |
|
from itertools import chain, combinations, product, permutations |
|
from sympy.core.add import Add |
|
from sympy.core.basic import Basic |
|
from sympy.core.cache import cacheit |
|
from sympy.core.containers import Tuple |
|
from sympy.core.decorators import sympify_method_args, sympify_return |
|
from sympy.core.function import Application, Derivative |
|
from sympy.core.kind import BooleanKind, NumberKind |
|
from sympy.core.numbers import Number |
|
from sympy.core.operations import LatticeOp |
|
from sympy.core.singleton import Singleton, S |
|
from sympy.core.sorting import ordered |
|
from sympy.core.sympify import _sympy_converter, _sympify, sympify |
|
from sympy.utilities.iterables import sift, ibin |
|
from sympy.utilities.misc import filldedent |
|
|
|
|
|
def as_Boolean(e): |
|
"""Like ``bool``, return the Boolean value of an expression, e, |
|
which can be any instance of :py:class:`~.Boolean` or ``bool``. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import true, false, nan |
|
>>> from sympy.logic.boolalg import as_Boolean |
|
>>> from sympy.abc import x |
|
>>> as_Boolean(0) is false |
|
True |
|
>>> as_Boolean(1) is true |
|
True |
|
>>> as_Boolean(x) |
|
x |
|
>>> as_Boolean(2) |
|
Traceback (most recent call last): |
|
... |
|
TypeError: expecting bool or Boolean, not `2`. |
|
>>> as_Boolean(nan) |
|
Traceback (most recent call last): |
|
... |
|
TypeError: expecting bool or Boolean, not `nan`. |
|
|
|
""" |
|
from sympy.core.symbol import Symbol |
|
if e == True: |
|
return true |
|
if e == False: |
|
return false |
|
if isinstance(e, Symbol): |
|
z = e.is_zero |
|
if z is None: |
|
return e |
|
return false if z else true |
|
if isinstance(e, Boolean): |
|
return e |
|
raise TypeError('expecting bool or Boolean, not `%s`.' % e) |
|
|
|
|
|
@sympify_method_args |
|
class Boolean(Basic): |
|
"""A Boolean object is an object for which logic operations make sense.""" |
|
|
|
__slots__ = () |
|
|
|
kind = BooleanKind |
|
|
|
if TYPE_CHECKING: |
|
|
|
def __new__(cls, *args: Basic | complex) -> Boolean: |
|
... |
|
|
|
@overload |
|
def subs(self, arg1: Mapping[Basic | complex, Boolean | complex], arg2: None=None) -> Boolean: ... |
|
@overload |
|
def subs(self, arg1: Iterable[tuple[Basic | complex, Boolean | complex]], arg2: None=None, **kwargs: Any) -> Boolean: ... |
|
@overload |
|
def subs(self, arg1: Boolean | complex, arg2: Boolean | complex) -> Boolean: ... |
|
@overload |
|
def subs(self, arg1: Mapping[Basic | complex, Basic | complex], arg2: None=None, **kwargs: Any) -> Basic: ... |
|
@overload |
|
def subs(self, arg1: Iterable[tuple[Basic | complex, Basic | complex]], arg2: None=None, **kwargs: Any) -> Basic: ... |
|
@overload |
|
def subs(self, arg1: Basic | complex, arg2: Basic | complex, **kwargs: Any) -> Basic: ... |
|
|
|
def subs(self, arg1: Mapping[Basic | complex, Basic | complex] | Basic | complex, |
|
arg2: Basic | complex | None = None, **kwargs: Any) -> Basic: |
|
... |
|
|
|
def simplify(self, **kwargs) -> Boolean: |
|
... |
|
|
|
@sympify_return([('other', 'Boolean')], NotImplemented) |
|
def __and__(self, other): |
|
return And(self, other) |
|
|
|
__rand__ = __and__ |
|
|
|
@sympify_return([('other', 'Boolean')], NotImplemented) |
|
def __or__(self, other): |
|
return Or(self, other) |
|
|
|
__ror__ = __or__ |
|
|
|
def __invert__(self): |
|
"""Overloading for ~""" |
|
return Not(self) |
|
|
|
@sympify_return([('other', 'Boolean')], NotImplemented) |
|
def __rshift__(self, other): |
|
return Implies(self, other) |
|
|
|
@sympify_return([('other', 'Boolean')], NotImplemented) |
|
def __lshift__(self, other): |
|
return Implies(other, self) |
|
|
|
__rrshift__ = __lshift__ |
|
__rlshift__ = __rshift__ |
|
|
|
@sympify_return([('other', 'Boolean')], NotImplemented) |
|
def __xor__(self, other): |
|
return Xor(self, other) |
|
|
|
__rxor__ = __xor__ |
|
|
|
def equals(self, other): |
|
""" |
|
Returns ``True`` if the given formulas have the same truth table. |
|
For two formulas to be equal they must have the same literals. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.abc import A, B, C |
|
>>> from sympy import And, Or, Not |
|
>>> (A >> B).equals(~B >> ~A) |
|
True |
|
>>> Not(And(A, B, C)).equals(And(Not(A), Not(B), Not(C))) |
|
False |
|
>>> Not(And(A, Not(A))).equals(Or(B, Not(B))) |
|
False |
|
|
|
""" |
|
from sympy.logic.inference import satisfiable |
|
from sympy.core.relational import Relational |
|
|
|
if self.has(Relational) or other.has(Relational): |
|
raise NotImplementedError('handling of relationals') |
|
return self.atoms() == other.atoms() and \ |
|
not satisfiable(Not(Equivalent(self, other))) |
|
|
|
def to_nnf(self, simplify=True): |
|
|
|
return self |
|
|
|
def as_set(self): |
|
""" |
|
Rewrites Boolean expression in terms of real sets. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import Symbol, Eq, Or, And |
|
>>> x = Symbol('x', real=True) |
|
>>> Eq(x, 0).as_set() |
|
{0} |
|
>>> (x > 0).as_set() |
|
Interval.open(0, oo) |
|
>>> And(-2 < x, x < 2).as_set() |
|
Interval.open(-2, 2) |
|
>>> Or(x < -2, 2 < x).as_set() |
|
Union(Interval.open(-oo, -2), Interval.open(2, oo)) |
|
|
|
""" |
|
from sympy.calculus.util import periodicity |
|
from sympy.core.relational import Relational |
|
|
|
free = self.free_symbols |
|
if len(free) == 1: |
|
x = free.pop() |
|
if x.kind is NumberKind: |
|
reps = {} |
|
for r in self.atoms(Relational): |
|
if periodicity(r, x) not in (0, None): |
|
s = r._eval_as_set() |
|
if s in (S.EmptySet, S.UniversalSet, S.Reals): |
|
reps[r] = s.as_relational(x) |
|
continue |
|
raise NotImplementedError(filldedent(''' |
|
as_set is not implemented for relationals |
|
with periodic solutions |
|
''')) |
|
new = self.subs(reps) |
|
if new.func != self.func: |
|
return new.as_set() |
|
else: |
|
return new._eval_as_set() |
|
|
|
return self._eval_as_set() |
|
else: |
|
raise NotImplementedError("Sorry, as_set has not yet been" |
|
" implemented for multivariate" |
|
" expressions") |
|
|
|
@property |
|
def binary_symbols(self): |
|
from sympy.core.relational import Eq, Ne |
|
return set().union(*[i.binary_symbols for i in self.args |
|
if i.is_Boolean or i.is_Symbol |
|
or isinstance(i, (Eq, Ne))]) |
|
|
|
def _eval_refine(self, assumptions): |
|
from sympy.assumptions import ask |
|
ret = ask(self, assumptions) |
|
if ret is True: |
|
return true |
|
elif ret is False: |
|
return false |
|
return None |
|
|
|
|
|
class BooleanAtom(Boolean): |
|
""" |
|
Base class of :py:class:`~.BooleanTrue` and :py:class:`~.BooleanFalse`. |
|
""" |
|
is_Boolean = True |
|
is_Atom = True |
|
_op_priority = 11 |
|
|
|
def simplify(self, *a, **kw): |
|
return self |
|
|
|
def expand(self, *a, **kw): |
|
return self |
|
|
|
@property |
|
def canonical(self): |
|
return self |
|
|
|
def _noop(self, other=None): |
|
raise TypeError('BooleanAtom not allowed in this context.') |
|
|
|
__add__ = _noop |
|
__radd__ = _noop |
|
__sub__ = _noop |
|
__rsub__ = _noop |
|
__mul__ = _noop |
|
__rmul__ = _noop |
|
__pow__ = _noop |
|
__rpow__ = _noop |
|
__truediv__ = _noop |
|
__rtruediv__ = _noop |
|
__mod__ = _noop |
|
__rmod__ = _noop |
|
_eval_power = _noop |
|
|
|
def __lt__(self, other): |
|
raise TypeError(filldedent(''' |
|
A Boolean argument can only be used in |
|
Eq and Ne; all other relationals expect |
|
real expressions. |
|
''')) |
|
|
|
__le__ = __lt__ |
|
__gt__ = __lt__ |
|
__ge__ = __lt__ |
|
|
|
|
|
def _eval_simplify(self, **kwargs): |
|
return self |
|
|
|
|
|
class BooleanTrue(BooleanAtom, metaclass=Singleton): |
|
""" |
|
SymPy version of ``True``, a singleton that can be accessed via ``S.true``. |
|
|
|
This is the SymPy version of ``True``, for use in the logic module. The |
|
primary advantage of using ``true`` instead of ``True`` is that shorthand Boolean |
|
operations like ``~`` and ``>>`` will work as expected on this class, whereas with |
|
True they act bitwise on 1. Functions in the logic module will return this |
|
class when they evaluate to true. |
|
|
|
Notes |
|
===== |
|
|
|
There is liable to be some confusion as to when ``True`` should |
|
be used and when ``S.true`` should be used in various contexts |
|
throughout SymPy. An important thing to remember is that |
|
``sympify(True)`` returns ``S.true``. This means that for the most |
|
part, you can just use ``True`` and it will automatically be converted |
|
to ``S.true`` when necessary, similar to how you can generally use 1 |
|
instead of ``S.One``. |
|
|
|
The rule of thumb is: |
|
|
|
"If the boolean in question can be replaced by an arbitrary symbolic |
|
``Boolean``, like ``Or(x, y)`` or ``x > 1``, use ``S.true``. |
|
Otherwise, use ``True``" |
|
|
|
In other words, use ``S.true`` only on those contexts where the |
|
boolean is being used as a symbolic representation of truth. |
|
For example, if the object ends up in the ``.args`` of any expression, |
|
then it must necessarily be ``S.true`` instead of ``True``, as |
|
elements of ``.args`` must be ``Basic``. On the other hand, |
|
``==`` is not a symbolic operation in SymPy, since it always returns |
|
``True`` or ``False``, and does so in terms of structural equality |
|
rather than mathematical, so it should return ``True``. The assumptions |
|
system should use ``True`` and ``False``. Aside from not satisfying |
|
the above rule of thumb, the assumptions system uses a three-valued logic |
|
(``True``, ``False``, ``None``), whereas ``S.true`` and ``S.false`` |
|
represent a two-valued logic. When in doubt, use ``True``. |
|
|
|
"``S.true == True is True``." |
|
|
|
While "``S.true is True``" is ``False``, "``S.true == True``" |
|
is ``True``, so if there is any doubt over whether a function or |
|
expression will return ``S.true`` or ``True``, just use ``==`` |
|
instead of ``is`` to do the comparison, and it will work in either |
|
case. Finally, for boolean flags, it's better to just use ``if x`` |
|
instead of ``if x is True``. To quote PEP 8: |
|
|
|
Do not compare boolean values to ``True`` or ``False`` |
|
using ``==``. |
|
|
|
* Yes: ``if greeting:`` |
|
* No: ``if greeting == True:`` |
|
* Worse: ``if greeting is True:`` |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import sympify, true, false, Or |
|
>>> sympify(True) |
|
True |
|
>>> _ is True, _ is true |
|
(False, True) |
|
|
|
>>> Or(true, false) |
|
True |
|
>>> _ is true |
|
True |
|
|
|
Python operators give a boolean result for true but a |
|
bitwise result for True |
|
|
|
>>> ~true, ~True # doctest: +SKIP |
|
(False, -2) |
|
>>> true >> true, True >> True |
|
(True, 0) |
|
|
|
See Also |
|
======== |
|
|
|
sympy.logic.boolalg.BooleanFalse |
|
|
|
""" |
|
def __bool__(self): |
|
return True |
|
|
|
def __hash__(self): |
|
return hash(True) |
|
|
|
def __eq__(self, other): |
|
if other is True: |
|
return True |
|
if other is False: |
|
return False |
|
return super().__eq__(other) |
|
|
|
@property |
|
def negated(self): |
|
return false |
|
|
|
def as_set(self): |
|
""" |
|
Rewrite logic operators and relationals in terms of real sets. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import true |
|
>>> true.as_set() |
|
UniversalSet |
|
|
|
""" |
|
return S.UniversalSet |
|
|
|
|
|
class BooleanFalse(BooleanAtom, metaclass=Singleton): |
|
""" |
|
SymPy version of ``False``, a singleton that can be accessed via ``S.false``. |
|
|
|
This is the SymPy version of ``False``, for use in the logic module. The |
|
primary advantage of using ``false`` instead of ``False`` is that shorthand |
|
Boolean operations like ``~`` and ``>>`` will work as expected on this class, |
|
whereas with ``False`` they act bitwise on 0. Functions in the logic module |
|
will return this class when they evaluate to false. |
|
|
|
Notes |
|
====== |
|
|
|
See the notes section in :py:class:`sympy.logic.boolalg.BooleanTrue` |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import sympify, true, false, Or |
|
>>> sympify(False) |
|
False |
|
>>> _ is False, _ is false |
|
(False, True) |
|
|
|
>>> Or(true, false) |
|
True |
|
>>> _ is true |
|
True |
|
|
|
Python operators give a boolean result for false but a |
|
bitwise result for False |
|
|
|
>>> ~false, ~False # doctest: +SKIP |
|
(True, -1) |
|
>>> false >> false, False >> False |
|
(True, 0) |
|
|
|
See Also |
|
======== |
|
|
|
sympy.logic.boolalg.BooleanTrue |
|
|
|
""" |
|
def __bool__(self): |
|
return False |
|
|
|
def __hash__(self): |
|
return hash(False) |
|
|
|
def __eq__(self, other): |
|
if other is True: |
|
return False |
|
if other is False: |
|
return True |
|
return super().__eq__(other) |
|
|
|
@property |
|
def negated(self): |
|
return true |
|
|
|
def as_set(self): |
|
""" |
|
Rewrite logic operators and relationals in terms of real sets. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import false |
|
>>> false.as_set() |
|
EmptySet |
|
""" |
|
return S.EmptySet |
|
|
|
|
|
true = BooleanTrue() |
|
false = BooleanFalse() |
|
|
|
|
|
|
|
|
|
S.true = true |
|
S.false = false |
|
|
|
_sympy_converter[bool] = lambda x: true if x else false |
|
|
|
|
|
class BooleanFunction(Application, Boolean): |
|
"""Boolean function is a function that lives in a boolean space |
|
It is used as base class for :py:class:`~.And`, :py:class:`~.Or`, |
|
:py:class:`~.Not`, etc. |
|
""" |
|
is_Boolean = True |
|
|
|
def _eval_simplify(self, **kwargs): |
|
rv = simplify_univariate(self) |
|
if not isinstance(rv, BooleanFunction): |
|
return rv.simplify(**kwargs) |
|
rv = rv.func(*[a.simplify(**kwargs) for a in rv.args]) |
|
return simplify_logic(rv) |
|
|
|
def simplify(self, **kwargs): |
|
from sympy.simplify.simplify import simplify |
|
return simplify(self, **kwargs) |
|
|
|
def __lt__(self, other): |
|
raise TypeError(filldedent(''' |
|
A Boolean argument can only be used in |
|
Eq and Ne; all other relationals expect |
|
real expressions. |
|
''')) |
|
__le__ = __lt__ |
|
__ge__ = __lt__ |
|
__gt__ = __lt__ |
|
|
|
@classmethod |
|
def binary_check_and_simplify(self, *args): |
|
return [as_Boolean(i) for i in args] |
|
|
|
def to_nnf(self, simplify=True): |
|
return self._to_nnf(*self.args, simplify=simplify) |
|
|
|
def to_anf(self, deep=True): |
|
return self._to_anf(*self.args, deep=deep) |
|
|
|
@classmethod |
|
def _to_nnf(cls, *args, **kwargs): |
|
simplify = kwargs.get('simplify', True) |
|
argset = set() |
|
for arg in args: |
|
if not is_literal(arg): |
|
arg = arg.to_nnf(simplify) |
|
if simplify: |
|
if isinstance(arg, cls): |
|
arg = arg.args |
|
else: |
|
arg = (arg,) |
|
for a in arg: |
|
if Not(a) in argset: |
|
return cls.zero |
|
argset.add(a) |
|
else: |
|
argset.add(arg) |
|
return cls(*argset) |
|
|
|
@classmethod |
|
def _to_anf(cls, *args, **kwargs): |
|
deep = kwargs.get('deep', True) |
|
new_args = [] |
|
for arg in args: |
|
if deep: |
|
if not is_literal(arg) or isinstance(arg, Not): |
|
arg = arg.to_anf(deep=deep) |
|
new_args.append(arg) |
|
return cls(*new_args, remove_true=False) |
|
|
|
|
|
def diff(self, *symbols, **assumptions): |
|
assumptions.setdefault("evaluate", True) |
|
return Derivative(self, *symbols, **assumptions) |
|
|
|
def _eval_derivative(self, x): |
|
if x in self.binary_symbols: |
|
from sympy.core.relational import Eq |
|
from sympy.functions.elementary.piecewise import Piecewise |
|
return Piecewise( |
|
(0, Eq(self.subs(x, 0), self.subs(x, 1))), |
|
(1, True)) |
|
elif x in self.free_symbols: |
|
|
|
|
|
pass |
|
else: |
|
return S.Zero |
|
|
|
|
|
class And(LatticeOp, BooleanFunction): |
|
""" |
|
Logical AND function. |
|
|
|
It evaluates its arguments in order, returning false immediately |
|
when an argument is false and true if they are all true. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.abc import x, y |
|
>>> from sympy import And |
|
>>> x & y |
|
x & y |
|
|
|
Notes |
|
===== |
|
|
|
The ``&`` operator is provided as a convenience, but note that its use |
|
here is different from its normal use in Python, which is bitwise |
|
and. Hence, ``And(a, b)`` and ``a & b`` will produce different results if |
|
``a`` and ``b`` are integers. |
|
|
|
>>> And(x, y).subs(x, 1) |
|
y |
|
|
|
""" |
|
zero = false |
|
identity = true |
|
|
|
nargs = None |
|
|
|
if TYPE_CHECKING: |
|
|
|
def __new__(cls, *args: Boolean | bool) -> Boolean: |
|
... |
|
|
|
@property |
|
def args(self) -> tuple[Boolean, ...]: |
|
... |
|
|
|
@classmethod |
|
def _new_args_filter(cls, args): |
|
args = BooleanFunction.binary_check_and_simplify(*args) |
|
args = LatticeOp._new_args_filter(args, And) |
|
newargs = [] |
|
rel = set() |
|
for x in ordered(args): |
|
if x.is_Relational: |
|
c = x.canonical |
|
if c in rel: |
|
continue |
|
elif c.negated.canonical in rel: |
|
return [false] |
|
else: |
|
rel.add(c) |
|
newargs.append(x) |
|
return newargs |
|
|
|
def _eval_subs(self, old, new): |
|
args = [] |
|
bad = None |
|
for i in self.args: |
|
try: |
|
i = i.subs(old, new) |
|
except TypeError: |
|
|
|
if bad is None: |
|
bad = i |
|
continue |
|
if i == False: |
|
return false |
|
elif i != True: |
|
args.append(i) |
|
if bad is not None: |
|
|
|
bad.subs(old, new) |
|
|
|
|
|
if isinstance(old, And): |
|
old_set = set(old.args) |
|
if old_set.issubset(args): |
|
args = set(args) - old_set |
|
args.add(new) |
|
|
|
return self.func(*args) |
|
|
|
def _eval_simplify(self, **kwargs): |
|
from sympy.core.relational import Equality, Relational |
|
from sympy.solvers.solveset import linear_coeffs |
|
|
|
rv = super()._eval_simplify(**kwargs) |
|
if not isinstance(rv, And): |
|
return rv |
|
|
|
|
|
|
|
Rel, nonRel = sift(rv.args, lambda i: isinstance(i, Relational), |
|
binary=True) |
|
if not Rel: |
|
return rv |
|
eqs, other = sift(Rel, lambda i: isinstance(i, Equality), binary=True) |
|
|
|
measure = kwargs['measure'] |
|
if eqs: |
|
ratio = kwargs['ratio'] |
|
reps = {} |
|
sifted = {} |
|
|
|
sifted = sift(ordered([ |
|
(i.free_symbols, i) for i in eqs]), |
|
lambda x: len(x[0])) |
|
eqs = [] |
|
nonlineqs = [] |
|
while 1 in sifted: |
|
for free, e in sifted.pop(1): |
|
x = free.pop() |
|
if (e.lhs != x or x in e.rhs.free_symbols) and x not in reps: |
|
try: |
|
m, b = linear_coeffs( |
|
Add(e.lhs, -e.rhs, evaluate=False), x) |
|
enew = e.func(x, -b/m) |
|
if measure(enew) <= ratio*measure(e): |
|
e = enew |
|
else: |
|
eqs.append(e) |
|
continue |
|
except ValueError: |
|
pass |
|
if x in reps: |
|
eqs.append(e.subs(x, reps[x])) |
|
elif e.lhs == x and x not in e.rhs.free_symbols: |
|
reps[x] = e.rhs |
|
eqs.append(e) |
|
else: |
|
|
|
nonlineqs.append(e) |
|
resifted = defaultdict(list) |
|
for k in sifted: |
|
for f, e in sifted[k]: |
|
e = e.xreplace(reps) |
|
f = e.free_symbols |
|
resifted[len(f)].append((f, e)) |
|
sifted = resifted |
|
for k in sifted: |
|
eqs.extend([e for f, e in sifted[k]]) |
|
nonlineqs = [ei.subs(reps) for ei in nonlineqs] |
|
other = [ei.subs(reps) for ei in other] |
|
rv = rv.func(*([i.canonical for i in (eqs + nonlineqs + other)] + nonRel)) |
|
patterns = _simplify_patterns_and() |
|
threeterm_patterns = _simplify_patterns_and3() |
|
return _apply_patternbased_simplification(rv, patterns, |
|
measure, false, |
|
threeterm_patterns=threeterm_patterns) |
|
|
|
def _eval_as_set(self): |
|
from sympy.sets.sets import Intersection |
|
return Intersection(*[arg.as_set() for arg in self.args]) |
|
|
|
def _eval_rewrite_as_Nor(self, *args, **kwargs): |
|
return Nor(*[Not(arg) for arg in self.args]) |
|
|
|
def to_anf(self, deep=True): |
|
if deep: |
|
result = And._to_anf(*self.args, deep=deep) |
|
return distribute_xor_over_and(result) |
|
return self |
|
|
|
|
|
class Or(LatticeOp, BooleanFunction): |
|
""" |
|
Logical OR function |
|
|
|
It evaluates its arguments in order, returning true immediately |
|
when an argument is true, and false if they are all false. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.abc import x, y |
|
>>> from sympy import Or |
|
>>> x | y |
|
x | y |
|
|
|
Notes |
|
===== |
|
|
|
The ``|`` operator is provided as a convenience, but note that its use |
|
here is different from its normal use in Python, which is bitwise |
|
or. Hence, ``Or(a, b)`` and ``a | b`` will return different things if |
|
``a`` and ``b`` are integers. |
|
|
|
>>> Or(x, y).subs(x, 0) |
|
y |
|
|
|
""" |
|
zero = true |
|
identity = false |
|
|
|
if TYPE_CHECKING: |
|
|
|
def __new__(cls, *args: Boolean | bool) -> Boolean: |
|
... |
|
|
|
@property |
|
def args(self) -> tuple[Boolean, ...]: |
|
... |
|
|
|
@classmethod |
|
def _new_args_filter(cls, args): |
|
newargs = [] |
|
rel = [] |
|
args = BooleanFunction.binary_check_and_simplify(*args) |
|
for x in args: |
|
if x.is_Relational: |
|
c = x.canonical |
|
if c in rel: |
|
continue |
|
nc = c.negated.canonical |
|
if any(r == nc for r in rel): |
|
return [true] |
|
rel.append(c) |
|
newargs.append(x) |
|
return LatticeOp._new_args_filter(newargs, Or) |
|
|
|
def _eval_subs(self, old, new): |
|
args = [] |
|
bad = None |
|
for i in self.args: |
|
try: |
|
i = i.subs(old, new) |
|
except TypeError: |
|
|
|
if bad is None: |
|
bad = i |
|
continue |
|
if i == True: |
|
return true |
|
elif i != False: |
|
args.append(i) |
|
if bad is not None: |
|
|
|
bad.subs(old, new) |
|
|
|
|
|
if isinstance(old, Or): |
|
old_set = set(old.args) |
|
if old_set.issubset(args): |
|
args = set(args) - old_set |
|
args.add(new) |
|
|
|
return self.func(*args) |
|
|
|
def _eval_as_set(self): |
|
from sympy.sets.sets import Union |
|
return Union(*[arg.as_set() for arg in self.args]) |
|
|
|
def _eval_rewrite_as_Nand(self, *args, **kwargs): |
|
return Nand(*[Not(arg) for arg in self.args]) |
|
|
|
def _eval_simplify(self, **kwargs): |
|
from sympy.core.relational import Le, Ge, Eq |
|
lege = self.atoms(Le, Ge) |
|
if lege: |
|
reps = {i: self.func( |
|
Eq(i.lhs, i.rhs), i.strict) for i in lege} |
|
return self.xreplace(reps)._eval_simplify(**kwargs) |
|
|
|
rv = super()._eval_simplify(**kwargs) |
|
if not isinstance(rv, Or): |
|
return rv |
|
patterns = _simplify_patterns_or() |
|
return _apply_patternbased_simplification(rv, patterns, |
|
kwargs['measure'], true) |
|
|
|
def to_anf(self, deep=True): |
|
args = range(1, len(self.args) + 1) |
|
args = (combinations(self.args, j) for j in args) |
|
args = chain.from_iterable(args) |
|
args = (And(*arg) for arg in args) |
|
args = (to_anf(x, deep=deep) if deep else x for x in args) |
|
return Xor(*list(args), remove_true=False) |
|
|
|
|
|
class Not(BooleanFunction): |
|
""" |
|
Logical Not function (negation) |
|
|
|
|
|
Returns ``true`` if the statement is ``false`` or ``False``. |
|
Returns ``false`` if the statement is ``true`` or ``True``. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import Not, And, Or |
|
>>> from sympy.abc import x, A, B |
|
>>> Not(True) |
|
False |
|
>>> Not(False) |
|
True |
|
>>> Not(And(True, False)) |
|
True |
|
>>> Not(Or(True, False)) |
|
False |
|
>>> Not(And(And(True, x), Or(x, False))) |
|
~x |
|
>>> ~x |
|
~x |
|
>>> Not(And(Or(A, B), Or(~A, ~B))) |
|
~((A | B) & (~A | ~B)) |
|
|
|
Notes |
|
===== |
|
|
|
- The ``~`` operator is provided as a convenience, but note that its use |
|
here is different from its normal use in Python, which is bitwise |
|
not. In particular, ``~a`` and ``Not(a)`` will be different if ``a`` is |
|
an integer. Furthermore, since bools in Python subclass from ``int``, |
|
``~True`` is the same as ``~1`` which is ``-2``, which has a boolean |
|
value of True. To avoid this issue, use the SymPy boolean types |
|
``true`` and ``false``. |
|
|
|
- As of Python 3.12, the bitwise not operator ``~`` used on a |
|
Python ``bool`` is deprecated and will emit a warning. |
|
|
|
>>> from sympy import true |
|
>>> ~True # doctest: +SKIP |
|
-2 |
|
>>> ~true |
|
False |
|
|
|
""" |
|
|
|
is_Not = True |
|
|
|
@classmethod |
|
def eval(cls, arg): |
|
if isinstance(arg, Number) or arg in (True, False): |
|
return false if arg else true |
|
if arg.is_Not: |
|
return arg.args[0] |
|
|
|
if arg.is_Relational: |
|
return arg.negated |
|
|
|
def _eval_as_set(self): |
|
""" |
|
Rewrite logic operators and relationals in terms of real sets. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import Not, Symbol |
|
>>> x = Symbol('x') |
|
>>> Not(x > 0).as_set() |
|
Interval(-oo, 0) |
|
""" |
|
return self.args[0].as_set().complement(S.Reals) |
|
|
|
def to_nnf(self, simplify=True): |
|
if is_literal(self): |
|
return self |
|
|
|
expr = self.args[0] |
|
|
|
func, args = expr.func, expr.args |
|
|
|
if func == And: |
|
return Or._to_nnf(*[Not(arg) for arg in args], simplify=simplify) |
|
|
|
if func == Or: |
|
return And._to_nnf(*[Not(arg) for arg in args], simplify=simplify) |
|
|
|
if func == Implies: |
|
a, b = args |
|
return And._to_nnf(a, Not(b), simplify=simplify) |
|
|
|
if func == Equivalent: |
|
return And._to_nnf(Or(*args), Or(*[Not(arg) for arg in args]), |
|
simplify=simplify) |
|
|
|
if func == Xor: |
|
result = [] |
|
for i in range(1, len(args)+1, 2): |
|
for neg in combinations(args, i): |
|
clause = [Not(s) if s in neg else s for s in args] |
|
result.append(Or(*clause)) |
|
return And._to_nnf(*result, simplify=simplify) |
|
|
|
if func == ITE: |
|
a, b, c = args |
|
return And._to_nnf(Or(a, Not(c)), Or(Not(a), Not(b)), simplify=simplify) |
|
|
|
raise ValueError("Illegal operator %s in expression" % func) |
|
|
|
def to_anf(self, deep=True): |
|
return Xor._to_anf(true, self.args[0], deep=deep) |
|
|
|
|
|
class Xor(BooleanFunction): |
|
""" |
|
Logical XOR (exclusive OR) function. |
|
|
|
|
|
Returns True if an odd number of the arguments are True and the rest are |
|
False. |
|
|
|
Returns False if an even number of the arguments are True and the rest are |
|
False. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Xor |
|
>>> from sympy import symbols |
|
>>> x, y = symbols('x y') |
|
>>> Xor(True, False) |
|
True |
|
>>> Xor(True, True) |
|
False |
|
>>> Xor(True, False, True, True, False) |
|
True |
|
>>> Xor(True, False, True, False) |
|
False |
|
>>> x ^ y |
|
x ^ y |
|
|
|
Notes |
|
===== |
|
|
|
The ``^`` operator is provided as a convenience, but note that its use |
|
here is different from its normal use in Python, which is bitwise xor. In |
|
particular, ``a ^ b`` and ``Xor(a, b)`` will be different if ``a`` and |
|
``b`` are integers. |
|
|
|
>>> Xor(x, y).subs(y, 0) |
|
x |
|
|
|
""" |
|
def __new__(cls, *args, remove_true=True, **kwargs): |
|
argset = set() |
|
obj = super().__new__(cls, *args, **kwargs) |
|
for arg in obj._args: |
|
if isinstance(arg, Number) or arg in (True, False): |
|
if arg: |
|
arg = true |
|
else: |
|
continue |
|
if isinstance(arg, Xor): |
|
for a in arg.args: |
|
argset.remove(a) if a in argset else argset.add(a) |
|
elif arg in argset: |
|
argset.remove(arg) |
|
else: |
|
argset.add(arg) |
|
rel = [(r, r.canonical, r.negated.canonical) |
|
for r in argset if r.is_Relational] |
|
odd = False |
|
remove = [] |
|
for i, (r, c, nc) in enumerate(rel): |
|
for j in range(i + 1, len(rel)): |
|
rj, cj = rel[j][:2] |
|
if cj == nc: |
|
odd = not odd |
|
break |
|
elif cj == c: |
|
break |
|
else: |
|
continue |
|
remove.append((r, rj)) |
|
if odd: |
|
argset.remove(true) if true in argset else argset.add(true) |
|
for a, b in remove: |
|
argset.remove(a) |
|
argset.remove(b) |
|
if len(argset) == 0: |
|
return false |
|
elif len(argset) == 1: |
|
return argset.pop() |
|
elif True in argset and remove_true: |
|
argset.remove(True) |
|
return Not(Xor(*argset)) |
|
else: |
|
obj._args = tuple(ordered(argset)) |
|
obj._argset = frozenset(argset) |
|
return obj |
|
|
|
|
|
|
|
@property |
|
@cacheit |
|
def args(self): |
|
return tuple(ordered(self._argset)) |
|
|
|
def to_nnf(self, simplify=True): |
|
args = [] |
|
for i in range(0, len(self.args)+1, 2): |
|
for neg in combinations(self.args, i): |
|
clause = [Not(s) if s in neg else s for s in self.args] |
|
args.append(Or(*clause)) |
|
return And._to_nnf(*args, simplify=simplify) |
|
|
|
def _eval_rewrite_as_Or(self, *args, **kwargs): |
|
a = self.args |
|
return Or(*[_convert_to_varsSOP(x, self.args) |
|
for x in _get_odd_parity_terms(len(a))]) |
|
|
|
def _eval_rewrite_as_And(self, *args, **kwargs): |
|
a = self.args |
|
return And(*[_convert_to_varsPOS(x, self.args) |
|
for x in _get_even_parity_terms(len(a))]) |
|
|
|
def _eval_simplify(self, **kwargs): |
|
|
|
|
|
|
|
rv = self.func(*[a.simplify(**kwargs) for a in self.args]) |
|
rv = rv.to_anf() |
|
if not isinstance(rv, Xor): |
|
return rv |
|
patterns = _simplify_patterns_xor() |
|
return _apply_patternbased_simplification(rv, patterns, |
|
kwargs['measure'], None) |
|
|
|
def _eval_subs(self, old, new): |
|
|
|
|
|
if isinstance(old, Xor): |
|
old_set = set(old.args) |
|
if old_set.issubset(self.args): |
|
args = set(self.args) - old_set |
|
args.add(new) |
|
return self.func(*args) |
|
|
|
|
|
class Nand(BooleanFunction): |
|
""" |
|
Logical NAND function. |
|
|
|
It evaluates its arguments in order, giving True immediately if any |
|
of them are False, and False if they are all True. |
|
|
|
Returns True if any of the arguments are False |
|
Returns False if all arguments are True |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Nand |
|
>>> from sympy import symbols |
|
>>> x, y = symbols('x y') |
|
>>> Nand(False, True) |
|
True |
|
>>> Nand(True, True) |
|
False |
|
>>> Nand(x, y) |
|
~(x & y) |
|
|
|
""" |
|
@classmethod |
|
def eval(cls, *args): |
|
return Not(And(*args)) |
|
|
|
|
|
class Nor(BooleanFunction): |
|
""" |
|
Logical NOR function. |
|
|
|
It evaluates its arguments in order, giving False immediately if any |
|
of them are True, and True if they are all False. |
|
|
|
Returns False if any argument is True |
|
Returns True if all arguments are False |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Nor |
|
>>> from sympy import symbols |
|
>>> x, y = symbols('x y') |
|
|
|
>>> Nor(True, False) |
|
False |
|
>>> Nor(True, True) |
|
False |
|
>>> Nor(False, True) |
|
False |
|
>>> Nor(False, False) |
|
True |
|
>>> Nor(x, y) |
|
~(x | y) |
|
|
|
""" |
|
@classmethod |
|
def eval(cls, *args): |
|
return Not(Or(*args)) |
|
|
|
|
|
class Xnor(BooleanFunction): |
|
""" |
|
Logical XNOR function. |
|
|
|
Returns False if an odd number of the arguments are True and the rest are |
|
False. |
|
|
|
Returns True if an even number of the arguments are True and the rest are |
|
False. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Xnor |
|
>>> from sympy import symbols |
|
>>> x, y = symbols('x y') |
|
>>> Xnor(True, False) |
|
False |
|
>>> Xnor(True, True) |
|
True |
|
>>> Xnor(True, False, True, True, False) |
|
False |
|
>>> Xnor(True, False, True, False) |
|
True |
|
|
|
""" |
|
@classmethod |
|
def eval(cls, *args): |
|
return Not(Xor(*args)) |
|
|
|
|
|
class Implies(BooleanFunction): |
|
r""" |
|
Logical implication. |
|
|
|
A implies B is equivalent to if A then B. Mathematically, it is written |
|
as `A \Rightarrow B` and is equivalent to `\neg A \vee B` or ``~A | B``. |
|
|
|
Accepts two Boolean arguments; A and B. |
|
Returns False if A is True and B is False |
|
Returns True otherwise. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Implies |
|
>>> from sympy import symbols |
|
>>> x, y = symbols('x y') |
|
|
|
>>> Implies(True, False) |
|
False |
|
>>> Implies(False, False) |
|
True |
|
>>> Implies(True, True) |
|
True |
|
>>> Implies(False, True) |
|
True |
|
>>> x >> y |
|
Implies(x, y) |
|
>>> y << x |
|
Implies(x, y) |
|
|
|
Notes |
|
===== |
|
|
|
The ``>>`` and ``<<`` operators are provided as a convenience, but note |
|
that their use here is different from their normal use in Python, which is |
|
bit shifts. Hence, ``Implies(a, b)`` and ``a >> b`` will return different |
|
things if ``a`` and ``b`` are integers. In particular, since Python |
|
considers ``True`` and ``False`` to be integers, ``True >> True`` will be |
|
the same as ``1 >> 1``, i.e., 0, which has a truth value of False. To |
|
avoid this issue, use the SymPy objects ``true`` and ``false``. |
|
|
|
>>> from sympy import true, false |
|
>>> True >> False |
|
1 |
|
>>> true >> false |
|
False |
|
|
|
""" |
|
@classmethod |
|
def eval(cls, *args): |
|
try: |
|
newargs = [] |
|
for x in args: |
|
if isinstance(x, Number) or x in (0, 1): |
|
newargs.append(bool(x)) |
|
else: |
|
newargs.append(x) |
|
A, B = newargs |
|
except ValueError: |
|
raise ValueError( |
|
"%d operand(s) used for an Implies " |
|
"(pairs are required): %s" % (len(args), str(args))) |
|
if A in (True, False) or B in (True, False): |
|
return Or(Not(A), B) |
|
elif A == B: |
|
return true |
|
elif A.is_Relational and B.is_Relational: |
|
if A.canonical == B.canonical: |
|
return true |
|
if A.negated.canonical == B.canonical: |
|
return B |
|
else: |
|
return Basic.__new__(cls, *args) |
|
|
|
def to_nnf(self, simplify=True): |
|
a, b = self.args |
|
return Or._to_nnf(Not(a), b, simplify=simplify) |
|
|
|
def to_anf(self, deep=True): |
|
a, b = self.args |
|
return Xor._to_anf(true, a, And(a, b), deep=deep) |
|
|
|
|
|
class Equivalent(BooleanFunction): |
|
""" |
|
Equivalence relation. |
|
|
|
``Equivalent(A, B)`` is True iff A and B are both True or both False. |
|
|
|
Returns True if all of the arguments are logically equivalent. |
|
Returns False otherwise. |
|
|
|
For two arguments, this is equivalent to :py:class:`~.Xnor`. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Equivalent, And |
|
>>> from sympy.abc import x |
|
>>> Equivalent(False, False, False) |
|
True |
|
>>> Equivalent(True, False, False) |
|
False |
|
>>> Equivalent(x, And(x, True)) |
|
True |
|
|
|
""" |
|
def __new__(cls, *args, **options): |
|
from sympy.core.relational import Relational |
|
args = [_sympify(arg) for arg in args] |
|
|
|
argset = set(args) |
|
for x in args: |
|
if isinstance(x, Number) or x in [True, False]: |
|
argset.discard(x) |
|
argset.add(bool(x)) |
|
rel = [] |
|
for r in argset: |
|
if isinstance(r, Relational): |
|
rel.append((r, r.canonical, r.negated.canonical)) |
|
remove = [] |
|
for i, (r, c, nc) in enumerate(rel): |
|
for j in range(i + 1, len(rel)): |
|
rj, cj = rel[j][:2] |
|
if cj == nc: |
|
return false |
|
elif cj == c: |
|
remove.append((r, rj)) |
|
break |
|
for a, b in remove: |
|
argset.remove(a) |
|
argset.remove(b) |
|
argset.add(True) |
|
if len(argset) <= 1: |
|
return true |
|
if True in argset: |
|
argset.discard(True) |
|
return And(*argset) |
|
if False in argset: |
|
argset.discard(False) |
|
return And(*[Not(arg) for arg in argset]) |
|
_args = frozenset(argset) |
|
obj = super().__new__(cls, _args) |
|
obj._argset = _args |
|
return obj |
|
|
|
|
|
|
|
@property |
|
@cacheit |
|
def args(self): |
|
return tuple(ordered(self._argset)) |
|
|
|
def to_nnf(self, simplify=True): |
|
args = [] |
|
for a, b in zip(self.args, self.args[1:]): |
|
args.append(Or(Not(a), b)) |
|
args.append(Or(Not(self.args[-1]), self.args[0])) |
|
return And._to_nnf(*args, simplify=simplify) |
|
|
|
def to_anf(self, deep=True): |
|
a = And(*self.args) |
|
b = And(*[to_anf(Not(arg), deep=False) for arg in self.args]) |
|
b = distribute_xor_over_and(b) |
|
return Xor._to_anf(a, b, deep=deep) |
|
|
|
|
|
class ITE(BooleanFunction): |
|
""" |
|
If-then-else clause. |
|
|
|
``ITE(A, B, C)`` evaluates and returns the result of B if A is true |
|
else it returns the result of C. All args must be Booleans. |
|
|
|
From a logic gate perspective, ITE corresponds to a 2-to-1 multiplexer, |
|
where A is the select signal. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import ITE, And, Xor, Or |
|
>>> from sympy.abc import x, y, z |
|
>>> ITE(True, False, True) |
|
False |
|
>>> ITE(Or(True, False), And(True, True), Xor(True, True)) |
|
True |
|
>>> ITE(x, y, z) |
|
ITE(x, y, z) |
|
>>> ITE(True, x, y) |
|
x |
|
>>> ITE(False, x, y) |
|
y |
|
>>> ITE(x, y, y) |
|
y |
|
|
|
Trying to use non-Boolean args will generate a TypeError: |
|
|
|
>>> ITE(True, [], ()) |
|
Traceback (most recent call last): |
|
... |
|
TypeError: expecting bool, Boolean or ITE, not `[]` |
|
|
|
""" |
|
def __new__(cls, *args, **kwargs): |
|
from sympy.core.relational import Eq, Ne |
|
if len(args) != 3: |
|
raise ValueError('expecting exactly 3 args') |
|
a, b, c = args |
|
|
|
if isinstance(a, (Eq, Ne)): |
|
|
|
|
|
|
|
b, c = map(as_Boolean, (b, c)) |
|
bin_syms = set().union(*[i.binary_symbols for i in (b, c)]) |
|
if len(set(a.args) - bin_syms) == 1: |
|
|
|
_a = a |
|
if a.lhs is true: |
|
a = a.rhs |
|
elif a.rhs is true: |
|
a = a.lhs |
|
elif a.lhs is false: |
|
a = Not(a.rhs) |
|
elif a.rhs is false: |
|
a = Not(a.lhs) |
|
else: |
|
|
|
a = false |
|
if isinstance(_a, Ne): |
|
a = Not(a) |
|
else: |
|
a, b, c = BooleanFunction.binary_check_and_simplify( |
|
a, b, c) |
|
rv = None |
|
if kwargs.get('evaluate', True): |
|
rv = cls.eval(a, b, c) |
|
if rv is None: |
|
rv = BooleanFunction.__new__(cls, a, b, c, evaluate=False) |
|
return rv |
|
|
|
@classmethod |
|
def eval(cls, *args): |
|
from sympy.core.relational import Eq, Ne |
|
|
|
a, b, c = args |
|
if isinstance(a, (Ne, Eq)): |
|
_a = a |
|
if true in a.args: |
|
a = a.lhs if a.rhs is true else a.rhs |
|
elif false in a.args: |
|
a = Not(a.lhs) if a.rhs is false else Not(a.rhs) |
|
else: |
|
_a = None |
|
if _a is not None and isinstance(_a, Ne): |
|
a = Not(a) |
|
if a is true: |
|
return b |
|
if a is false: |
|
return c |
|
if b == c: |
|
return b |
|
else: |
|
|
|
|
|
if b is true and c is false: |
|
return a |
|
if b is false and c is true: |
|
return Not(a) |
|
if [a, b, c] != args: |
|
return cls(a, b, c, evaluate=False) |
|
|
|
def to_nnf(self, simplify=True): |
|
a, b, c = self.args |
|
return And._to_nnf(Or(Not(a), b), Or(a, c), simplify=simplify) |
|
|
|
def _eval_as_set(self): |
|
return self.to_nnf().as_set() |
|
|
|
def _eval_rewrite_as_Piecewise(self, *args, **kwargs): |
|
from sympy.functions.elementary.piecewise import Piecewise |
|
return Piecewise((args[1], args[0]), (args[2], True)) |
|
|
|
|
|
class Exclusive(BooleanFunction): |
|
""" |
|
True if only one or no argument is true. |
|
|
|
``Exclusive(A, B, C)`` is equivalent to ``~(A & B) & ~(A & C) & ~(B & C)``. |
|
|
|
For two arguments, this is equivalent to :py:class:`~.Xor`. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Exclusive |
|
>>> Exclusive(False, False, False) |
|
True |
|
>>> Exclusive(False, True, False) |
|
True |
|
>>> Exclusive(False, True, True) |
|
False |
|
|
|
""" |
|
@classmethod |
|
def eval(cls, *args): |
|
and_args = [] |
|
for a, b in combinations(args, 2): |
|
and_args.append(Not(And(a, b))) |
|
return And(*and_args) |
|
|
|
|
|
|
|
|
|
|
|
def conjuncts(expr): |
|
"""Return a list of the conjuncts in ``expr``. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import conjuncts |
|
>>> from sympy.abc import A, B |
|
>>> conjuncts(A & B) |
|
frozenset({A, B}) |
|
>>> conjuncts(A | B) |
|
frozenset({A | B}) |
|
|
|
""" |
|
return And.make_args(expr) |
|
|
|
|
|
def disjuncts(expr): |
|
"""Return a list of the disjuncts in ``expr``. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import disjuncts |
|
>>> from sympy.abc import A, B |
|
>>> disjuncts(A | B) |
|
frozenset({A, B}) |
|
>>> disjuncts(A & B) |
|
frozenset({A & B}) |
|
|
|
""" |
|
return Or.make_args(expr) |
|
|
|
|
|
def distribute_and_over_or(expr): |
|
""" |
|
Given a sentence ``expr`` consisting of conjunctions and disjunctions |
|
of literals, return an equivalent sentence in CNF. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import distribute_and_over_or, And, Or, Not |
|
>>> from sympy.abc import A, B, C |
|
>>> distribute_and_over_or(Or(A, And(Not(B), Not(C)))) |
|
(A | ~B) & (A | ~C) |
|
|
|
""" |
|
return _distribute((expr, And, Or)) |
|
|
|
|
|
def distribute_or_over_and(expr): |
|
""" |
|
Given a sentence ``expr`` consisting of conjunctions and disjunctions |
|
of literals, return an equivalent sentence in DNF. |
|
|
|
Note that the output is NOT simplified. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import distribute_or_over_and, And, Or, Not |
|
>>> from sympy.abc import A, B, C |
|
>>> distribute_or_over_and(And(Or(Not(A), B), C)) |
|
(B & C) | (C & ~A) |
|
|
|
""" |
|
return _distribute((expr, Or, And)) |
|
|
|
|
|
def distribute_xor_over_and(expr): |
|
""" |
|
Given a sentence ``expr`` consisting of conjunction and |
|
exclusive disjunctions of literals, return an |
|
equivalent exclusive disjunction. |
|
|
|
Note that the output is NOT simplified. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import distribute_xor_over_and, And, Xor, Not |
|
>>> from sympy.abc import A, B, C |
|
>>> distribute_xor_over_and(And(Xor(Not(A), B), C)) |
|
(B & C) ^ (C & ~A) |
|
""" |
|
return _distribute((expr, Xor, And)) |
|
|
|
|
|
def _distribute(info): |
|
""" |
|
Distributes ``info[1]`` over ``info[2]`` with respect to ``info[0]``. |
|
""" |
|
if isinstance(info[0], info[2]): |
|
for arg in info[0].args: |
|
if isinstance(arg, info[1]): |
|
conj = arg |
|
break |
|
else: |
|
return info[0] |
|
rest = info[2](*[a for a in info[0].args if a is not conj]) |
|
return info[1](*list(map(_distribute, |
|
[(info[2](c, rest), info[1], info[2]) |
|
for c in conj.args])), remove_true=False) |
|
elif isinstance(info[0], info[1]): |
|
return info[1](*list(map(_distribute, |
|
[(x, info[1], info[2]) |
|
for x in info[0].args])), |
|
remove_true=False) |
|
else: |
|
return info[0] |
|
|
|
|
|
def to_anf(expr, deep=True): |
|
r""" |
|
Converts expr to Algebraic Normal Form (ANF). |
|
|
|
ANF is a canonical normal form, which means that two |
|
equivalent formulas will convert to the same ANF. |
|
|
|
A logical expression is in ANF if it has the form |
|
|
|
.. math:: 1 \oplus a \oplus b \oplus ab \oplus abc |
|
|
|
i.e. it can be: |
|
- purely true, |
|
- purely false, |
|
- conjunction of variables, |
|
- exclusive disjunction. |
|
|
|
The exclusive disjunction can only contain true, variables |
|
or conjunction of variables. No negations are permitted. |
|
|
|
If ``deep`` is ``False``, arguments of the boolean |
|
expression are considered variables, i.e. only the |
|
top-level expression is converted to ANF. |
|
|
|
Examples |
|
======== |
|
>>> from sympy.logic.boolalg import And, Or, Not, Implies, Equivalent |
|
>>> from sympy.logic.boolalg import to_anf |
|
>>> from sympy.abc import A, B, C |
|
>>> to_anf(Not(A)) |
|
A ^ True |
|
>>> to_anf(And(Or(A, B), Not(C))) |
|
A ^ B ^ (A & B) ^ (A & C) ^ (B & C) ^ (A & B & C) |
|
>>> to_anf(Implies(Not(A), Equivalent(B, C)), deep=False) |
|
True ^ ~A ^ (~A & (Equivalent(B, C))) |
|
|
|
""" |
|
expr = sympify(expr) |
|
|
|
if is_anf(expr): |
|
return expr |
|
return expr.to_anf(deep=deep) |
|
|
|
|
|
def to_nnf(expr, simplify=True): |
|
""" |
|
Converts ``expr`` to Negation Normal Form (NNF). |
|
|
|
A logical expression is in NNF if it |
|
contains only :py:class:`~.And`, :py:class:`~.Or` and :py:class:`~.Not`, |
|
and :py:class:`~.Not` is applied only to literals. |
|
If ``simplify`` is ``True``, the result contains no redundant clauses. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.abc import A, B, C, D |
|
>>> from sympy.logic.boolalg import Not, Equivalent, to_nnf |
|
>>> to_nnf(Not((~A & ~B) | (C & D))) |
|
(A | B) & (~C | ~D) |
|
>>> to_nnf(Equivalent(A >> B, B >> A)) |
|
(A | ~B | (A & ~B)) & (B | ~A | (B & ~A)) |
|
|
|
""" |
|
if is_nnf(expr, simplify): |
|
return expr |
|
return expr.to_nnf(simplify) |
|
|
|
|
|
def to_cnf(expr, simplify=False, force=False): |
|
""" |
|
Convert a propositional logical sentence ``expr`` to conjunctive normal |
|
form: ``((A | ~B | ...) & (B | C | ...) & ...)``. |
|
If ``simplify`` is ``True``, ``expr`` is evaluated to its simplest CNF |
|
form using the Quine-McCluskey algorithm; this may take a long |
|
time. If there are more than 8 variables the ``force`` flag must be set |
|
to ``True`` to simplify (default is ``False``). |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import to_cnf |
|
>>> from sympy.abc import A, B, D |
|
>>> to_cnf(~(A | B) | D) |
|
(D | ~A) & (D | ~B) |
|
>>> to_cnf((A | B) & (A | ~A), True) |
|
A | B |
|
|
|
""" |
|
expr = sympify(expr) |
|
if not isinstance(expr, BooleanFunction): |
|
return expr |
|
|
|
if simplify: |
|
if not force and len(_find_predicates(expr)) > 8: |
|
raise ValueError(filldedent(''' |
|
To simplify a logical expression with more |
|
than 8 variables may take a long time and requires |
|
the use of `force=True`.''')) |
|
return simplify_logic(expr, 'cnf', True, force=force) |
|
|
|
|
|
if is_cnf(expr): |
|
return expr |
|
|
|
expr = eliminate_implications(expr) |
|
res = distribute_and_over_or(expr) |
|
|
|
return res |
|
|
|
|
|
def to_dnf(expr, simplify=False, force=False): |
|
""" |
|
Convert a propositional logical sentence ``expr`` to disjunctive normal |
|
form: ``((A & ~B & ...) | (B & C & ...) | ...)``. |
|
If ``simplify`` is ``True``, ``expr`` is evaluated to its simplest DNF form using |
|
the Quine-McCluskey algorithm; this may take a long |
|
time. If there are more than 8 variables, the ``force`` flag must be set to |
|
``True`` to simplify (default is ``False``). |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import to_dnf |
|
>>> from sympy.abc import A, B, C |
|
>>> to_dnf(B & (A | C)) |
|
(A & B) | (B & C) |
|
>>> to_dnf((A & B) | (A & ~B) | (B & C) | (~B & C), True) |
|
A | C |
|
|
|
""" |
|
expr = sympify(expr) |
|
if not isinstance(expr, BooleanFunction): |
|
return expr |
|
|
|
if simplify: |
|
if not force and len(_find_predicates(expr)) > 8: |
|
raise ValueError(filldedent(''' |
|
To simplify a logical expression with more |
|
than 8 variables may take a long time and requires |
|
the use of `force=True`.''')) |
|
return simplify_logic(expr, 'dnf', True, force=force) |
|
|
|
|
|
if is_dnf(expr): |
|
return expr |
|
|
|
expr = eliminate_implications(expr) |
|
return distribute_or_over_and(expr) |
|
|
|
|
|
def is_anf(expr): |
|
r""" |
|
Checks if ``expr`` is in Algebraic Normal Form (ANF). |
|
|
|
A logical expression is in ANF if it has the form |
|
|
|
.. math:: 1 \oplus a \oplus b \oplus ab \oplus abc |
|
|
|
i.e. it is purely true, purely false, conjunction of |
|
variables or exclusive disjunction. The exclusive |
|
disjunction can only contain true, variables or |
|
conjunction of variables. No negations are permitted. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import And, Not, Xor, true, is_anf |
|
>>> from sympy.abc import A, B, C |
|
>>> is_anf(true) |
|
True |
|
>>> is_anf(A) |
|
True |
|
>>> is_anf(And(A, B, C)) |
|
True |
|
>>> is_anf(Xor(A, Not(B))) |
|
False |
|
|
|
""" |
|
expr = sympify(expr) |
|
|
|
if is_literal(expr) and not isinstance(expr, Not): |
|
return True |
|
|
|
if isinstance(expr, And): |
|
for arg in expr.args: |
|
if not arg.is_Symbol: |
|
return False |
|
return True |
|
|
|
elif isinstance(expr, Xor): |
|
for arg in expr.args: |
|
if isinstance(arg, And): |
|
for a in arg.args: |
|
if not a.is_Symbol: |
|
return False |
|
elif is_literal(arg): |
|
if isinstance(arg, Not): |
|
return False |
|
else: |
|
return False |
|
return True |
|
|
|
else: |
|
return False |
|
|
|
|
|
def is_nnf(expr, simplified=True): |
|
""" |
|
Checks if ``expr`` is in Negation Normal Form (NNF). |
|
|
|
A logical expression is in NNF if it |
|
contains only :py:class:`~.And`, :py:class:`~.Or` and :py:class:`~.Not`, |
|
and :py:class:`~.Not` is applied only to literals. |
|
If ``simplified`` is ``True``, checks if result contains no redundant clauses. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.abc import A, B, C |
|
>>> from sympy.logic.boolalg import Not, is_nnf |
|
>>> is_nnf(A & B | ~C) |
|
True |
|
>>> is_nnf((A | ~A) & (B | C)) |
|
False |
|
>>> is_nnf((A | ~A) & (B | C), False) |
|
True |
|
>>> is_nnf(Not(A & B) | C) |
|
False |
|
>>> is_nnf((A >> B) & (B >> A)) |
|
False |
|
|
|
""" |
|
|
|
expr = sympify(expr) |
|
if is_literal(expr): |
|
return True |
|
|
|
stack = [expr] |
|
|
|
while stack: |
|
expr = stack.pop() |
|
if expr.func in (And, Or): |
|
if simplified: |
|
args = expr.args |
|
for arg in args: |
|
if Not(arg) in args: |
|
return False |
|
stack.extend(expr.args) |
|
|
|
elif not is_literal(expr): |
|
return False |
|
|
|
return True |
|
|
|
|
|
def is_cnf(expr): |
|
""" |
|
Test whether or not an expression is in conjunctive normal form. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import is_cnf |
|
>>> from sympy.abc import A, B, C |
|
>>> is_cnf(A | B | C) |
|
True |
|
>>> is_cnf(A & B & C) |
|
True |
|
>>> is_cnf((A & B) | C) |
|
False |
|
|
|
""" |
|
return _is_form(expr, And, Or) |
|
|
|
|
|
def is_dnf(expr): |
|
""" |
|
Test whether or not an expression is in disjunctive normal form. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import is_dnf |
|
>>> from sympy.abc import A, B, C |
|
>>> is_dnf(A | B | C) |
|
True |
|
>>> is_dnf(A & B & C) |
|
True |
|
>>> is_dnf((A & B) | C) |
|
True |
|
>>> is_dnf(A & (B | C)) |
|
False |
|
|
|
""" |
|
return _is_form(expr, Or, And) |
|
|
|
|
|
def _is_form(expr, function1, function2): |
|
""" |
|
Test whether or not an expression is of the required form. |
|
|
|
""" |
|
expr = sympify(expr) |
|
|
|
vals = function1.make_args(expr) if isinstance(expr, function1) else [expr] |
|
for lit in vals: |
|
if isinstance(lit, function2): |
|
vals2 = function2.make_args(lit) if isinstance(lit, function2) else [lit] |
|
for l in vals2: |
|
if is_literal(l) is False: |
|
return False |
|
elif is_literal(lit) is False: |
|
return False |
|
|
|
return True |
|
|
|
|
|
def eliminate_implications(expr): |
|
""" |
|
Change :py:class:`~.Implies` and :py:class:`~.Equivalent` into |
|
:py:class:`~.And`, :py:class:`~.Or`, and :py:class:`~.Not`. |
|
That is, return an expression that is equivalent to ``expr``, but has only |
|
``&``, ``|``, and ``~`` as logical |
|
operators. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import Implies, Equivalent, \ |
|
eliminate_implications |
|
>>> from sympy.abc import A, B, C |
|
>>> eliminate_implications(Implies(A, B)) |
|
B | ~A |
|
>>> eliminate_implications(Equivalent(A, B)) |
|
(A | ~B) & (B | ~A) |
|
>>> eliminate_implications(Equivalent(A, B, C)) |
|
(A | ~C) & (B | ~A) & (C | ~B) |
|
|
|
""" |
|
return to_nnf(expr, simplify=False) |
|
|
|
|
|
def is_literal(expr): |
|
""" |
|
Returns True if expr is a literal, else False. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import Or, Q |
|
>>> from sympy.abc import A, B |
|
>>> from sympy.logic.boolalg import is_literal |
|
>>> is_literal(A) |
|
True |
|
>>> is_literal(~A) |
|
True |
|
>>> is_literal(Q.zero(A)) |
|
True |
|
>>> is_literal(A + B) |
|
True |
|
>>> is_literal(Or(A, B)) |
|
False |
|
|
|
""" |
|
from sympy.assumptions import AppliedPredicate |
|
|
|
if isinstance(expr, Not): |
|
return is_literal(expr.args[0]) |
|
elif expr in (True, False) or isinstance(expr, AppliedPredicate) or expr.is_Atom: |
|
return True |
|
elif not isinstance(expr, BooleanFunction) and all( |
|
(isinstance(expr, AppliedPredicate) or a.is_Atom) for a in expr.args): |
|
return True |
|
return False |
|
|
|
|
|
def to_int_repr(clauses, symbols): |
|
""" |
|
Takes clauses in CNF format and puts them into an integer representation. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import to_int_repr |
|
>>> from sympy.abc import x, y |
|
>>> to_int_repr([x | y, y], [x, y]) == [{1, 2}, {2}] |
|
True |
|
|
|
""" |
|
|
|
|
|
symbols = dict(zip(symbols, range(1, len(symbols) + 1))) |
|
|
|
def append_symbol(arg, symbols): |
|
if isinstance(arg, Not): |
|
return -symbols[arg.args[0]] |
|
else: |
|
return symbols[arg] |
|
|
|
return [{append_symbol(arg, symbols) for arg in Or.make_args(c)} |
|
for c in clauses] |
|
|
|
|
|
def term_to_integer(term): |
|
""" |
|
Return an integer corresponding to the base-2 digits given by *term*. |
|
|
|
Parameters |
|
========== |
|
|
|
term : a string or list of ones and zeros |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import term_to_integer |
|
>>> term_to_integer([1, 0, 0]) |
|
4 |
|
>>> term_to_integer('100') |
|
4 |
|
|
|
""" |
|
|
|
return int(''.join(list(map(str, list(term)))), 2) |
|
|
|
|
|
integer_to_term = ibin |
|
|
|
|
|
def truth_table(expr, variables, input=True): |
|
""" |
|
Return a generator of all possible configurations of the input variables, |
|
and the result of the boolean expression for those values. |
|
|
|
Parameters |
|
========== |
|
|
|
expr : Boolean expression |
|
|
|
variables : list of variables |
|
|
|
input : bool (default ``True``) |
|
Indicates whether to return the input combinations. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import truth_table |
|
>>> from sympy.abc import x,y |
|
>>> table = truth_table(x >> y, [x, y]) |
|
>>> for t in table: |
|
... print('{0} -> {1}'.format(*t)) |
|
[0, 0] -> True |
|
[0, 1] -> True |
|
[1, 0] -> False |
|
[1, 1] -> True |
|
|
|
>>> table = truth_table(x | y, [x, y]) |
|
>>> list(table) |
|
[([0, 0], False), ([0, 1], True), ([1, 0], True), ([1, 1], True)] |
|
|
|
If ``input`` is ``False``, ``truth_table`` returns only a list of truth values. |
|
In this case, the corresponding input values of variables can be |
|
deduced from the index of a given output. |
|
|
|
>>> from sympy.utilities.iterables import ibin |
|
>>> vars = [y, x] |
|
>>> values = truth_table(x >> y, vars, input=False) |
|
>>> values = list(values) |
|
>>> values |
|
[True, False, True, True] |
|
|
|
>>> for i, value in enumerate(values): |
|
... print('{0} -> {1}'.format(list(zip( |
|
... vars, ibin(i, len(vars)))), value)) |
|
[(y, 0), (x, 0)] -> True |
|
[(y, 0), (x, 1)] -> False |
|
[(y, 1), (x, 0)] -> True |
|
[(y, 1), (x, 1)] -> True |
|
|
|
""" |
|
variables = [sympify(v) for v in variables] |
|
|
|
expr = sympify(expr) |
|
if not isinstance(expr, BooleanFunction) and not is_literal(expr): |
|
return |
|
|
|
table = product((0, 1), repeat=len(variables)) |
|
for term in table: |
|
value = expr.xreplace(dict(zip(variables, term))) |
|
|
|
if input: |
|
yield list(term), value |
|
else: |
|
yield value |
|
|
|
|
|
def _check_pair(minterm1, minterm2): |
|
""" |
|
Checks if a pair of minterms differs by only one bit. If yes, returns |
|
index, else returns `-1`. |
|
""" |
|
|
|
|
|
index = -1 |
|
for x, i in enumerate(minterm1): |
|
if i != minterm2[x]: |
|
if index == -1: |
|
index = x |
|
else: |
|
return -1 |
|
return index |
|
|
|
|
|
def _convert_to_varsSOP(minterm, variables): |
|
""" |
|
Converts a term in the expansion of a function from binary to its |
|
variable form (for SOP). |
|
""" |
|
temp = [variables[n] if val == 1 else Not(variables[n]) |
|
for n, val in enumerate(minterm) if val != 3] |
|
return And(*temp) |
|
|
|
|
|
def _convert_to_varsPOS(maxterm, variables): |
|
""" |
|
Converts a term in the expansion of a function from binary to its |
|
variable form (for POS). |
|
""" |
|
temp = [variables[n] if val == 0 else Not(variables[n]) |
|
for n, val in enumerate(maxterm) if val != 3] |
|
return Or(*temp) |
|
|
|
|
|
def _convert_to_varsANF(term, variables): |
|
""" |
|
Converts a term in the expansion of a function from binary to its |
|
variable form (for ANF). |
|
|
|
Parameters |
|
========== |
|
|
|
term : list of 1's and 0's (complementation pattern) |
|
variables : list of variables |
|
|
|
""" |
|
temp = [variables[n] for n, t in enumerate(term) if t == 1] |
|
|
|
if not temp: |
|
return true |
|
|
|
return And(*temp) |
|
|
|
|
|
def _get_odd_parity_terms(n): |
|
""" |
|
Returns a list of lists, with all possible combinations of n zeros and ones |
|
with an odd number of ones. |
|
""" |
|
return [e for e in [ibin(i, n) for i in range(2**n)] if sum(e) % 2 == 1] |
|
|
|
|
|
def _get_even_parity_terms(n): |
|
""" |
|
Returns a list of lists, with all possible combinations of n zeros and ones |
|
with an even number of ones. |
|
""" |
|
return [e for e in [ibin(i, n) for i in range(2**n)] if sum(e) % 2 == 0] |
|
|
|
|
|
def _simplified_pairs(terms): |
|
""" |
|
Reduces a set of minterms, if possible, to a simplified set of minterms |
|
with one less variable in the terms using QM method. |
|
""" |
|
if not terms: |
|
return [] |
|
|
|
simplified_terms = [] |
|
todo = list(range(len(terms))) |
|
|
|
|
|
|
|
termdict = defaultdict(list) |
|
for n, term in enumerate(terms): |
|
ones = sum(1 for t in term if t == 1) |
|
termdict[ones].append(n) |
|
|
|
variables = len(terms[0]) |
|
for k in range(variables): |
|
for i in termdict[k]: |
|
for j in termdict[k+1]: |
|
index = _check_pair(terms[i], terms[j]) |
|
if index != -1: |
|
|
|
todo[i] = todo[j] = None |
|
|
|
newterm = terms[i][:] |
|
|
|
newterm[index] = 3 |
|
|
|
if newterm not in simplified_terms: |
|
simplified_terms.append(newterm) |
|
|
|
if simplified_terms: |
|
|
|
simplified_terms = _simplified_pairs(simplified_terms) |
|
|
|
|
|
simplified_terms.extend([terms[i] for i in todo if i is not None]) |
|
return simplified_terms |
|
|
|
|
|
def _rem_redundancy(l1, terms): |
|
""" |
|
After the truth table has been sufficiently simplified, use the prime |
|
implicant table method to recognize and eliminate redundant pairs, |
|
and return the essential arguments. |
|
""" |
|
|
|
if not terms: |
|
return [] |
|
|
|
nterms = len(terms) |
|
nl1 = len(l1) |
|
|
|
|
|
dommatrix = [[0]*nl1 for n in range(nterms)] |
|
colcount = [0]*nl1 |
|
rowcount = [0]*nterms |
|
for primei, prime in enumerate(l1): |
|
for termi, term in enumerate(terms): |
|
|
|
if all(t == 3 or t == mt for t, mt in zip(prime, term)): |
|
dommatrix[termi][primei] = 1 |
|
colcount[primei] += 1 |
|
rowcount[termi] += 1 |
|
|
|
|
|
anythingchanged = True |
|
|
|
while anythingchanged: |
|
anythingchanged = False |
|
|
|
for rowi in range(nterms): |
|
|
|
if rowcount[rowi]: |
|
row = dommatrix[rowi] |
|
for row2i in range(nterms): |
|
|
|
if rowi != row2i and rowcount[rowi] and (rowcount[rowi] <= rowcount[row2i]): |
|
row2 = dommatrix[row2i] |
|
if all(row2[n] >= row[n] for n in range(nl1)): |
|
|
|
rowcount[row2i] = 0 |
|
anythingchanged = True |
|
for primei, prime in enumerate(row2): |
|
if prime: |
|
|
|
dommatrix[row2i][primei] = 0 |
|
colcount[primei] -= 1 |
|
|
|
colcache = {} |
|
|
|
for coli in range(nl1): |
|
|
|
if colcount[coli]: |
|
if coli in colcache: |
|
col = colcache[coli] |
|
else: |
|
col = [dommatrix[i][coli] for i in range(nterms)] |
|
colcache[coli] = col |
|
for col2i in range(nl1): |
|
|
|
if coli != col2i and colcount[col2i] and (colcount[coli] >= colcount[col2i]): |
|
if col2i in colcache: |
|
col2 = colcache[col2i] |
|
else: |
|
col2 = [dommatrix[i][col2i] for i in range(nterms)] |
|
colcache[col2i] = col2 |
|
if all(col[n] >= col2[n] for n in range(nterms)): |
|
|
|
colcount[col2i] = 0 |
|
anythingchanged = True |
|
for termi, term in enumerate(col2): |
|
if term and dommatrix[termi][col2i]: |
|
|
|
dommatrix[termi][col2i] = 0 |
|
rowcount[termi] -= 1 |
|
|
|
if not anythingchanged: |
|
|
|
maxterms = 0 |
|
bestcolidx = -1 |
|
for coli in range(nl1): |
|
s = colcount[coli] |
|
if s > maxterms: |
|
bestcolidx = coli |
|
maxterms = s |
|
|
|
|
|
if bestcolidx != -1 and maxterms > 1: |
|
for primei, prime in enumerate(l1): |
|
if primei != bestcolidx: |
|
for termi, term in enumerate(colcache[bestcolidx]): |
|
if term and dommatrix[termi][primei]: |
|
|
|
dommatrix[termi][primei] = 0 |
|
anythingchanged = True |
|
rowcount[termi] -= 1 |
|
colcount[primei] -= 1 |
|
|
|
return [l1[i] for i in range(nl1) if colcount[i]] |
|
|
|
|
|
def _input_to_binlist(inputlist, variables): |
|
binlist = [] |
|
bits = len(variables) |
|
for val in inputlist: |
|
if isinstance(val, int): |
|
binlist.append(ibin(val, bits)) |
|
elif isinstance(val, dict): |
|
nonspecvars = list(variables) |
|
for key in val.keys(): |
|
nonspecvars.remove(key) |
|
for t in product((0, 1), repeat=len(nonspecvars)): |
|
d = dict(zip(nonspecvars, t)) |
|
d.update(val) |
|
binlist.append([d[v] for v in variables]) |
|
elif isinstance(val, (list, tuple)): |
|
if len(val) != bits: |
|
raise ValueError("Each term must contain {bits} bits as there are" |
|
"\n{bits} variables (or be an integer)." |
|
"".format(bits=bits)) |
|
binlist.append(list(val)) |
|
else: |
|
raise TypeError("A term list can only contain lists," |
|
" ints or dicts.") |
|
return binlist |
|
|
|
|
|
def SOPform(variables, minterms, dontcares=None): |
|
""" |
|
The SOPform function uses simplified_pairs and a redundant group- |
|
eliminating algorithm to convert the list of all input combos that |
|
generate '1' (the minterms) into the smallest sum-of-products form. |
|
|
|
The variables must be given as the first argument. |
|
|
|
Return a logical :py:class:`~.Or` function (i.e., the "sum of products" or |
|
"SOP" form) that gives the desired outcome. If there are inputs that can |
|
be ignored, pass them as a list, too. |
|
|
|
The result will be one of the (perhaps many) functions that satisfy |
|
the conditions. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic import SOPform |
|
>>> from sympy import symbols |
|
>>> w, x, y, z = symbols('w x y z') |
|
>>> minterms = [[0, 0, 0, 1], [0, 0, 1, 1], |
|
... [0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 1, 1]] |
|
>>> dontcares = [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 1]] |
|
>>> SOPform([w, x, y, z], minterms, dontcares) |
|
(y & z) | (~w & ~x) |
|
|
|
The terms can also be represented as integers: |
|
|
|
>>> minterms = [1, 3, 7, 11, 15] |
|
>>> dontcares = [0, 2, 5] |
|
>>> SOPform([w, x, y, z], minterms, dontcares) |
|
(y & z) | (~w & ~x) |
|
|
|
They can also be specified using dicts, which does not have to be fully |
|
specified: |
|
|
|
>>> minterms = [{w: 0, x: 1}, {y: 1, z: 1, x: 0}] |
|
>>> SOPform([w, x, y, z], minterms) |
|
(x & ~w) | (y & z & ~x) |
|
|
|
Or a combination: |
|
|
|
>>> minterms = [4, 7, 11, [1, 1, 1, 1]] |
|
>>> dontcares = [{w : 0, x : 0, y: 0}, 5] |
|
>>> SOPform([w, x, y, z], minterms, dontcares) |
|
(w & y & z) | (~w & ~y) | (x & z & ~w) |
|
|
|
See also |
|
======== |
|
|
|
POSform |
|
|
|
References |
|
========== |
|
|
|
.. [1] https://en.wikipedia.org/wiki/Quine-McCluskey_algorithm |
|
.. [2] https://en.wikipedia.org/wiki/Don%27t-care_term |
|
|
|
""" |
|
if not minterms: |
|
return false |
|
|
|
variables = tuple(map(sympify, variables)) |
|
|
|
|
|
minterms = _input_to_binlist(minterms, variables) |
|
dontcares = _input_to_binlist((dontcares or []), variables) |
|
for d in dontcares: |
|
if d in minterms: |
|
raise ValueError('%s in minterms is also in dontcares' % d) |
|
|
|
return _sop_form(variables, minterms, dontcares) |
|
|
|
|
|
def _sop_form(variables, minterms, dontcares): |
|
new = _simplified_pairs(minterms + dontcares) |
|
essential = _rem_redundancy(new, minterms) |
|
return Or(*[_convert_to_varsSOP(x, variables) for x in essential]) |
|
|
|
|
|
def POSform(variables, minterms, dontcares=None): |
|
""" |
|
The POSform function uses simplified_pairs and a redundant-group |
|
eliminating algorithm to convert the list of all input combinations |
|
that generate '1' (the minterms) into the smallest product-of-sums form. |
|
|
|
The variables must be given as the first argument. |
|
|
|
Return a logical :py:class:`~.And` function (i.e., the "product of sums" |
|
or "POS" form) that gives the desired outcome. If there are inputs that can |
|
be ignored, pass them as a list, too. |
|
|
|
The result will be one of the (perhaps many) functions that satisfy |
|
the conditions. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic import POSform |
|
>>> from sympy import symbols |
|
>>> w, x, y, z = symbols('w x y z') |
|
>>> minterms = [[0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 1, 1], |
|
... [1, 0, 1, 1], [1, 1, 1, 1]] |
|
>>> dontcares = [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 1]] |
|
>>> POSform([w, x, y, z], minterms, dontcares) |
|
z & (y | ~w) |
|
|
|
The terms can also be represented as integers: |
|
|
|
>>> minterms = [1, 3, 7, 11, 15] |
|
>>> dontcares = [0, 2, 5] |
|
>>> POSform([w, x, y, z], minterms, dontcares) |
|
z & (y | ~w) |
|
|
|
They can also be specified using dicts, which does not have to be fully |
|
specified: |
|
|
|
>>> minterms = [{w: 0, x: 1}, {y: 1, z: 1, x: 0}] |
|
>>> POSform([w, x, y, z], minterms) |
|
(x | y) & (x | z) & (~w | ~x) |
|
|
|
Or a combination: |
|
|
|
>>> minterms = [4, 7, 11, [1, 1, 1, 1]] |
|
>>> dontcares = [{w : 0, x : 0, y: 0}, 5] |
|
>>> POSform([w, x, y, z], minterms, dontcares) |
|
(w | x) & (y | ~w) & (z | ~y) |
|
|
|
See also |
|
======== |
|
|
|
SOPform |
|
|
|
References |
|
========== |
|
|
|
.. [1] https://en.wikipedia.org/wiki/Quine-McCluskey_algorithm |
|
.. [2] https://en.wikipedia.org/wiki/Don%27t-care_term |
|
|
|
""" |
|
if not minterms: |
|
return false |
|
|
|
variables = tuple(map(sympify, variables)) |
|
minterms = _input_to_binlist(minterms, variables) |
|
dontcares = _input_to_binlist((dontcares or []), variables) |
|
for d in dontcares: |
|
if d in minterms: |
|
raise ValueError('%s in minterms is also in dontcares' % d) |
|
|
|
maxterms = [] |
|
for t in product((0, 1), repeat=len(variables)): |
|
t = list(t) |
|
if (t not in minterms) and (t not in dontcares): |
|
maxterms.append(t) |
|
|
|
new = _simplified_pairs(maxterms + dontcares) |
|
essential = _rem_redundancy(new, maxterms) |
|
return And(*[_convert_to_varsPOS(x, variables) for x in essential]) |
|
|
|
|
|
def ANFform(variables, truthvalues): |
|
""" |
|
The ANFform function converts the list of truth values to |
|
Algebraic Normal Form (ANF). |
|
|
|
The variables must be given as the first argument. |
|
|
|
Return True, False, logical :py:class:`~.And` function (i.e., the |
|
"Zhegalkin monomial") or logical :py:class:`~.Xor` function (i.e., |
|
the "Zhegalkin polynomial"). When True and False |
|
are represented by 1 and 0, respectively, then |
|
:py:class:`~.And` is multiplication and :py:class:`~.Xor` is addition. |
|
|
|
Formally a "Zhegalkin monomial" is the product (logical |
|
And) of a finite set of distinct variables, including |
|
the empty set whose product is denoted 1 (True). |
|
A "Zhegalkin polynomial" is the sum (logical Xor) of a |
|
set of Zhegalkin monomials, with the empty set denoted |
|
by 0 (False). |
|
|
|
Parameters |
|
========== |
|
|
|
variables : list of variables |
|
truthvalues : list of 1's and 0's (result column of truth table) |
|
|
|
Examples |
|
======== |
|
>>> from sympy.logic.boolalg import ANFform |
|
>>> from sympy.abc import x, y |
|
>>> ANFform([x], [1, 0]) |
|
x ^ True |
|
>>> ANFform([x, y], [0, 1, 1, 1]) |
|
x ^ y ^ (x & y) |
|
|
|
References |
|
========== |
|
|
|
.. [1] https://en.wikipedia.org/wiki/Zhegalkin_polynomial |
|
|
|
""" |
|
|
|
n_vars = len(variables) |
|
n_values = len(truthvalues) |
|
|
|
if n_values != 2 ** n_vars: |
|
raise ValueError("The number of truth values must be equal to 2^%d, " |
|
"got %d" % (n_vars, n_values)) |
|
|
|
variables = tuple(map(sympify, variables)) |
|
|
|
coeffs = anf_coeffs(truthvalues) |
|
terms = [] |
|
|
|
for i, t in enumerate(product((0, 1), repeat=n_vars)): |
|
if coeffs[i] == 1: |
|
terms.append(t) |
|
|
|
return Xor(*[_convert_to_varsANF(x, variables) for x in terms], |
|
remove_true=False) |
|
|
|
|
|
def anf_coeffs(truthvalues): |
|
""" |
|
Convert a list of truth values of some boolean expression |
|
to the list of coefficients of the polynomial mod 2 (exclusive |
|
disjunction) representing the boolean expression in ANF |
|
(i.e., the "Zhegalkin polynomial"). |
|
|
|
There are `2^n` possible Zhegalkin monomials in `n` variables, since |
|
each monomial is fully specified by the presence or absence of |
|
each variable. |
|
|
|
We can enumerate all the monomials. For example, boolean |
|
function with four variables ``(a, b, c, d)`` can contain |
|
up to `2^4 = 16` monomials. The 13-th monomial is the |
|
product ``a & b & d``, because 13 in binary is 1, 1, 0, 1. |
|
|
|
A given monomial's presence or absence in a polynomial corresponds |
|
to that monomial's coefficient being 1 or 0 respectively. |
|
|
|
Examples |
|
======== |
|
>>> from sympy.logic.boolalg import anf_coeffs, bool_monomial, Xor |
|
>>> from sympy.abc import a, b, c |
|
>>> truthvalues = [0, 1, 1, 0, 0, 1, 0, 1] |
|
>>> coeffs = anf_coeffs(truthvalues) |
|
>>> coeffs |
|
[0, 1, 1, 0, 0, 0, 1, 0] |
|
>>> polynomial = Xor(*[ |
|
... bool_monomial(k, [a, b, c]) |
|
... for k, coeff in enumerate(coeffs) if coeff == 1 |
|
... ]) |
|
>>> polynomial |
|
b ^ c ^ (a & b) |
|
|
|
""" |
|
|
|
s = '{:b}'.format(len(truthvalues)) |
|
n = len(s) - 1 |
|
|
|
if len(truthvalues) != 2**n: |
|
raise ValueError("The number of truth values must be a power of two, " |
|
"got %d" % len(truthvalues)) |
|
|
|
coeffs = [[v] for v in truthvalues] |
|
|
|
for i in range(n): |
|
tmp = [] |
|
for j in range(2 ** (n-i-1)): |
|
tmp.append(coeffs[2*j] + |
|
list(map(lambda x, y: x^y, coeffs[2*j], coeffs[2*j+1]))) |
|
coeffs = tmp |
|
|
|
return coeffs[0] |
|
|
|
|
|
def bool_minterm(k, variables): |
|
""" |
|
Return the k-th minterm. |
|
|
|
Minterms are numbered by a binary encoding of the complementation |
|
pattern of the variables. This convention assigns the value 1 to |
|
the direct form and 0 to the complemented form. |
|
|
|
Parameters |
|
========== |
|
|
|
k : int or list of 1's and 0's (complementation pattern) |
|
variables : list of variables |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import bool_minterm |
|
>>> from sympy.abc import x, y, z |
|
>>> bool_minterm([1, 0, 1], [x, y, z]) |
|
x & z & ~y |
|
>>> bool_minterm(6, [x, y, z]) |
|
x & y & ~z |
|
|
|
References |
|
========== |
|
|
|
.. [1] https://en.wikipedia.org/wiki/Canonical_normal_form#Indexing_minterms |
|
|
|
""" |
|
if isinstance(k, int): |
|
k = ibin(k, len(variables)) |
|
variables = tuple(map(sympify, variables)) |
|
return _convert_to_varsSOP(k, variables) |
|
|
|
|
|
def bool_maxterm(k, variables): |
|
""" |
|
Return the k-th maxterm. |
|
|
|
Each maxterm is assigned an index based on the opposite |
|
conventional binary encoding used for minterms. The maxterm |
|
convention assigns the value 0 to the direct form and 1 to |
|
the complemented form. |
|
|
|
Parameters |
|
========== |
|
|
|
k : int or list of 1's and 0's (complementation pattern) |
|
variables : list of variables |
|
|
|
Examples |
|
======== |
|
>>> from sympy.logic.boolalg import bool_maxterm |
|
>>> from sympy.abc import x, y, z |
|
>>> bool_maxterm([1, 0, 1], [x, y, z]) |
|
y | ~x | ~z |
|
>>> bool_maxterm(6, [x, y, z]) |
|
z | ~x | ~y |
|
|
|
References |
|
========== |
|
|
|
.. [1] https://en.wikipedia.org/wiki/Canonical_normal_form#Indexing_maxterms |
|
|
|
""" |
|
if isinstance(k, int): |
|
k = ibin(k, len(variables)) |
|
variables = tuple(map(sympify, variables)) |
|
return _convert_to_varsPOS(k, variables) |
|
|
|
|
|
def bool_monomial(k, variables): |
|
""" |
|
Return the k-th monomial. |
|
|
|
Monomials are numbered by a binary encoding of the presence and |
|
absences of the variables. This convention assigns the value |
|
1 to the presence of variable and 0 to the absence of variable. |
|
|
|
Each boolean function can be uniquely represented by a |
|
Zhegalkin Polynomial (Algebraic Normal Form). The Zhegalkin |
|
Polynomial of the boolean function with `n` variables can contain |
|
up to `2^n` monomials. We can enumerate all the monomials. |
|
Each monomial is fully specified by the presence or absence |
|
of each variable. |
|
|
|
For example, boolean function with four variables ``(a, b, c, d)`` |
|
can contain up to `2^4 = 16` monomials. The 13-th monomial is the |
|
product ``a & b & d``, because 13 in binary is 1, 1, 0, 1. |
|
|
|
Parameters |
|
========== |
|
|
|
k : int or list of 1's and 0's |
|
variables : list of variables |
|
|
|
Examples |
|
======== |
|
>>> from sympy.logic.boolalg import bool_monomial |
|
>>> from sympy.abc import x, y, z |
|
>>> bool_monomial([1, 0, 1], [x, y, z]) |
|
x & z |
|
>>> bool_monomial(6, [x, y, z]) |
|
x & y |
|
|
|
""" |
|
if isinstance(k, int): |
|
k = ibin(k, len(variables)) |
|
variables = tuple(map(sympify, variables)) |
|
return _convert_to_varsANF(k, variables) |
|
|
|
|
|
def _find_predicates(expr): |
|
"""Helper to find logical predicates in BooleanFunctions. |
|
|
|
A logical predicate is defined here as anything within a BooleanFunction |
|
that is not a BooleanFunction itself. |
|
|
|
""" |
|
if not isinstance(expr, BooleanFunction): |
|
return {expr} |
|
return set().union(*(map(_find_predicates, expr.args))) |
|
|
|
|
|
def simplify_logic(expr, form=None, deep=True, force=False, dontcare=None): |
|
""" |
|
This function simplifies a boolean function to its simplified version |
|
in SOP or POS form. The return type is an :py:class:`~.Or` or |
|
:py:class:`~.And` object in SymPy. |
|
|
|
Parameters |
|
========== |
|
|
|
expr : Boolean |
|
|
|
form : string (``'cnf'`` or ``'dnf'``) or ``None`` (default). |
|
If ``'cnf'`` or ``'dnf'``, the simplest expression in the corresponding |
|
normal form is returned; if ``None``, the answer is returned |
|
according to the form with fewest args (in CNF by default). |
|
|
|
deep : bool (default ``True``) |
|
Indicates whether to recursively simplify any |
|
non-boolean functions contained within the input. |
|
|
|
force : bool (default ``False``) |
|
As the simplifications require exponential time in the number |
|
of variables, there is by default a limit on expressions with |
|
8 variables. When the expression has more than 8 variables |
|
only symbolical simplification (controlled by ``deep``) is |
|
made. By setting ``force`` to ``True``, this limit is removed. Be |
|
aware that this can lead to very long simplification times. |
|
|
|
dontcare : Boolean |
|
Optimize expression under the assumption that inputs where this |
|
expression is true are don't care. This is useful in e.g. Piecewise |
|
conditions, where later conditions do not need to consider inputs that |
|
are converted by previous conditions. For example, if a previous |
|
condition is ``And(A, B)``, the simplification of expr can be made |
|
with don't cares for ``And(A, B)``. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic import simplify_logic |
|
>>> from sympy.abc import x, y, z |
|
>>> b = (~x & ~y & ~z) | ( ~x & ~y & z) |
|
>>> simplify_logic(b) |
|
~x & ~y |
|
>>> simplify_logic(x | y, dontcare=y) |
|
x |
|
|
|
References |
|
========== |
|
|
|
.. [1] https://en.wikipedia.org/wiki/Don%27t-care_term |
|
|
|
""" |
|
|
|
if form not in (None, 'cnf', 'dnf'): |
|
raise ValueError("form can be cnf or dnf only") |
|
expr = sympify(expr) |
|
|
|
|
|
if form: |
|
form_ok = False |
|
if form == 'cnf': |
|
form_ok = is_cnf(expr) |
|
elif form == 'dnf': |
|
form_ok = is_dnf(expr) |
|
|
|
if form_ok and all(is_literal(a) |
|
for a in expr.args): |
|
return expr |
|
from sympy.core.relational import Relational |
|
if deep: |
|
variables = expr.atoms(Relational) |
|
from sympy.simplify.simplify import simplify |
|
s = tuple(map(simplify, variables)) |
|
expr = expr.xreplace(dict(zip(variables, s))) |
|
if not isinstance(expr, BooleanFunction): |
|
return expr |
|
|
|
|
|
repl = {} |
|
undo = {} |
|
from sympy.core.symbol import Dummy |
|
variables = expr.atoms(Relational) |
|
if dontcare is not None: |
|
dontcare = sympify(dontcare) |
|
variables.update(dontcare.atoms(Relational)) |
|
while variables: |
|
var = variables.pop() |
|
if var.is_Relational: |
|
d = Dummy() |
|
undo[d] = var |
|
repl[var] = d |
|
nvar = var.negated |
|
if nvar in variables: |
|
repl[nvar] = Not(d) |
|
variables.remove(nvar) |
|
|
|
expr = expr.xreplace(repl) |
|
|
|
if dontcare is not None: |
|
dontcare = dontcare.xreplace(repl) |
|
|
|
|
|
variables = _find_predicates(expr) |
|
if not force and len(variables) > 8: |
|
return expr.xreplace(undo) |
|
if dontcare is not None: |
|
|
|
dcvariables = _find_predicates(dontcare) |
|
variables.update(dcvariables) |
|
|
|
if not force and len(variables) > 8: |
|
variables = _find_predicates(expr) |
|
dontcare = None |
|
|
|
c, v = sift(ordered(variables), lambda x: x in (True, False), binary=True) |
|
variables = c + v |
|
|
|
c = [1 if i == True else 0 for i in c] |
|
truthtable = _get_truthtable(v, expr, c) |
|
if dontcare is not None: |
|
dctruthtable = _get_truthtable(v, dontcare, c) |
|
truthtable = [t for t in truthtable if t not in dctruthtable] |
|
else: |
|
dctruthtable = [] |
|
big = len(truthtable) >= (2 ** (len(variables) - 1)) |
|
if form == 'dnf' or form is None and big: |
|
return _sop_form(variables, truthtable, dctruthtable).xreplace(undo) |
|
return POSform(variables, truthtable, dctruthtable).xreplace(undo) |
|
|
|
|
|
def _get_truthtable(variables, expr, const): |
|
""" Return a list of all combinations leading to a True result for ``expr``. |
|
""" |
|
_variables = variables.copy() |
|
def _get_tt(inputs): |
|
if _variables: |
|
v = _variables.pop() |
|
tab = [[i[0].xreplace({v: false}), [0] + i[1]] for i in inputs if i[0] is not false] |
|
tab.extend([[i[0].xreplace({v: true}), [1] + i[1]] for i in inputs if i[0] is not false]) |
|
return _get_tt(tab) |
|
return inputs |
|
res = [const + k[1] for k in _get_tt([[expr, []]]) if k[0]] |
|
if res == [[]]: |
|
return [] |
|
else: |
|
return res |
|
|
|
|
|
def _finger(eq): |
|
""" |
|
Assign a 5-item fingerprint to each symbol in the equation: |
|
[ |
|
# of times it appeared as a Symbol; |
|
# of times it appeared as a Not(symbol); |
|
# of times it appeared as a Symbol in an And or Or; |
|
# of times it appeared as a Not(Symbol) in an And or Or; |
|
a sorted tuple of tuples, (i, j, k), where i is the number of arguments |
|
in an And or Or with which it appeared as a Symbol, and j is |
|
the number of arguments that were Not(Symbol); k is the number |
|
of times that (i, j) was seen. |
|
] |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic.boolalg import _finger as finger |
|
>>> from sympy import And, Or, Not, Xor, to_cnf, symbols |
|
>>> from sympy.abc import a, b, x, y |
|
>>> eq = Or(And(Not(y), a), And(Not(y), b), And(x, y)) |
|
>>> dict(finger(eq)) |
|
{(0, 0, 1, 0, ((2, 0, 1),)): [x], |
|
(0, 0, 1, 0, ((2, 1, 1),)): [a, b], |
|
(0, 0, 1, 2, ((2, 0, 1),)): [y]} |
|
>>> dict(finger(x & ~y)) |
|
{(0, 1, 0, 0, ()): [y], (1, 0, 0, 0, ()): [x]} |
|
|
|
In the following, the (5, 2, 6) means that there were 6 Or |
|
functions in which a symbol appeared as itself amongst 5 arguments in |
|
which there were also 2 negated symbols, e.g. ``(a0 | a1 | a2 | ~a3 | ~a4)`` |
|
is counted once for a0, a1 and a2. |
|
|
|
>>> dict(finger(to_cnf(Xor(*symbols('a:5'))))) |
|
{(0, 0, 8, 8, ((5, 0, 1), (5, 2, 6), (5, 4, 1))): [a0, a1, a2, a3, a4]} |
|
|
|
The equation must not have more than one level of nesting: |
|
|
|
>>> dict(finger(And(Or(x, y), y))) |
|
{(0, 0, 1, 0, ((2, 0, 1),)): [x], (1, 0, 1, 0, ((2, 0, 1),)): [y]} |
|
>>> dict(finger(And(Or(x, And(a, x)), y))) |
|
Traceback (most recent call last): |
|
... |
|
NotImplementedError: unexpected level of nesting |
|
|
|
So y and x have unique fingerprints, but a and b do not. |
|
""" |
|
f = eq.free_symbols |
|
d = dict(list(zip(f, [[0]*4 + [defaultdict(int)] for fi in f]))) |
|
for a in eq.args: |
|
if a.is_Symbol: |
|
d[a][0] += 1 |
|
elif a.is_Not: |
|
d[a.args[0]][1] += 1 |
|
else: |
|
o = len(a.args), sum(isinstance(ai, Not) for ai in a.args) |
|
for ai in a.args: |
|
if ai.is_Symbol: |
|
d[ai][2] += 1 |
|
d[ai][-1][o] += 1 |
|
elif ai.is_Not: |
|
d[ai.args[0]][3] += 1 |
|
else: |
|
raise NotImplementedError('unexpected level of nesting') |
|
inv = defaultdict(list) |
|
for k, v in ordered(iter(d.items())): |
|
v[-1] = tuple(sorted([i + (j,) for i, j in v[-1].items()])) |
|
inv[tuple(v)].append(k) |
|
return inv |
|
|
|
|
|
def bool_map(bool1, bool2): |
|
""" |
|
Return the simplified version of *bool1*, and the mapping of variables |
|
that makes the two expressions *bool1* and *bool2* represent the same |
|
logical behaviour for some correspondence between the variables |
|
of each. |
|
If more than one mappings of this sort exist, one of them |
|
is returned. |
|
|
|
For example, ``And(x, y)`` is logically equivalent to ``And(a, b)`` for |
|
the mapping ``{x: a, y: b}`` or ``{x: b, y: a}``. |
|
If no such mapping exists, return ``False``. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy import SOPform, bool_map, Or, And, Not, Xor |
|
>>> from sympy.abc import w, x, y, z, a, b, c, d |
|
>>> function1 = SOPform([x, z, y],[[1, 0, 1], [0, 0, 1]]) |
|
>>> function2 = SOPform([a, b, c],[[1, 0, 1], [1, 0, 0]]) |
|
>>> bool_map(function1, function2) |
|
(y & ~z, {y: a, z: b}) |
|
|
|
The results are not necessarily unique, but they are canonical. Here, |
|
``(w, z)`` could be ``(a, d)`` or ``(d, a)``: |
|
|
|
>>> eq = Or(And(Not(y), w), And(Not(y), z), And(x, y)) |
|
>>> eq2 = Or(And(Not(c), a), And(Not(c), d), And(b, c)) |
|
>>> bool_map(eq, eq2) |
|
((x & y) | (w & ~y) | (z & ~y), {w: a, x: b, y: c, z: d}) |
|
>>> eq = And(Xor(a, b), c, And(c,d)) |
|
>>> bool_map(eq, eq.subs(c, x)) |
|
(c & d & (a | b) & (~a | ~b), {a: a, b: b, c: d, d: x}) |
|
|
|
""" |
|
|
|
def match(function1, function2): |
|
"""Return the mapping that equates variables between two |
|
simplified boolean expressions if possible. |
|
|
|
By "simplified" we mean that a function has been denested |
|
and is either an And (or an Or) whose arguments are either |
|
symbols (x), negated symbols (Not(x)), or Or (or an And) whose |
|
arguments are only symbols or negated symbols. For example, |
|
``And(x, Not(y), Or(w, Not(z)))``. |
|
|
|
Basic.match is not robust enough (see issue 4835) so this is |
|
a workaround that is valid for simplified boolean expressions |
|
""" |
|
|
|
|
|
if function1.__class__ != function2.__class__: |
|
return None |
|
if len(function1.args) != len(function2.args): |
|
return None |
|
if function1.is_Symbol: |
|
return {function1: function2} |
|
|
|
|
|
f1 = _finger(function1) |
|
f2 = _finger(function2) |
|
|
|
|
|
if len(f1) != len(f2): |
|
return False |
|
|
|
|
|
matchdict = {} |
|
for k in f1.keys(): |
|
if k not in f2: |
|
return False |
|
if len(f1[k]) != len(f2[k]): |
|
return False |
|
for i, x in enumerate(f1[k]): |
|
matchdict[x] = f2[k][i] |
|
return matchdict |
|
|
|
a = simplify_logic(bool1) |
|
b = simplify_logic(bool2) |
|
m = match(a, b) |
|
if m: |
|
return a, m |
|
return m |
|
|
|
|
|
def _apply_patternbased_simplification(rv, patterns, measure, |
|
dominatingvalue, |
|
replacementvalue=None, |
|
threeterm_patterns=None): |
|
""" |
|
Replace patterns of Relational |
|
|
|
Parameters |
|
========== |
|
|
|
rv : Expr |
|
Boolean expression |
|
|
|
patterns : tuple |
|
Tuple of tuples, with (pattern to simplify, simplified pattern) with |
|
two terms. |
|
|
|
measure : function |
|
Simplification measure. |
|
|
|
dominatingvalue : Boolean or ``None`` |
|
The dominating value for the function of consideration. |
|
For example, for :py:class:`~.And` ``S.false`` is dominating. |
|
As soon as one expression is ``S.false`` in :py:class:`~.And`, |
|
the whole expression is ``S.false``. |
|
|
|
replacementvalue : Boolean or ``None``, optional |
|
The resulting value for the whole expression if one argument |
|
evaluates to ``dominatingvalue``. |
|
For example, for :py:class:`~.Nand` ``S.false`` is dominating, but |
|
in this case the resulting value is ``S.true``. Default is ``None``. |
|
If ``replacementvalue`` is ``None`` and ``dominatingvalue`` is not |
|
``None``, ``replacementvalue = dominatingvalue``. |
|
|
|
threeterm_patterns : tuple, optional |
|
Tuple of tuples, with (pattern to simplify, simplified pattern) with |
|
three terms. |
|
|
|
""" |
|
from sympy.core.relational import Relational, _canonical |
|
|
|
if replacementvalue is None and dominatingvalue is not None: |
|
replacementvalue = dominatingvalue |
|
|
|
Rel, nonRel = sift(rv.args, lambda i: isinstance(i, Relational), |
|
binary=True) |
|
if len(Rel) <= 1: |
|
return rv |
|
Rel, nonRealRel = sift(Rel, lambda i: not any(s.is_real is False |
|
for s in i.free_symbols), |
|
binary=True) |
|
Rel = [i.canonical for i in Rel] |
|
|
|
if threeterm_patterns and len(Rel) >= 3: |
|
Rel = _apply_patternbased_threeterm_simplification(Rel, |
|
threeterm_patterns, rv.func, dominatingvalue, |
|
replacementvalue, measure) |
|
|
|
Rel = _apply_patternbased_twoterm_simplification(Rel, patterns, |
|
rv.func, dominatingvalue, replacementvalue, measure) |
|
|
|
rv = rv.func(*([_canonical(i) for i in ordered(Rel)] |
|
+ nonRel + nonRealRel)) |
|
return rv |
|
|
|
|
|
def _apply_patternbased_twoterm_simplification(Rel, patterns, func, |
|
dominatingvalue, |
|
replacementvalue, |
|
measure): |
|
""" Apply pattern-based two-term simplification.""" |
|
from sympy.functions.elementary.miscellaneous import Min, Max |
|
from sympy.core.relational import Ge, Gt, _Inequality |
|
changed = True |
|
while changed and len(Rel) >= 2: |
|
changed = False |
|
|
|
Rel = [r.reversed if isinstance(r, (Ge, Gt)) else r for r in Rel] |
|
|
|
Rel = list(ordered(Rel)) |
|
|
|
rtmp = [(r, ) if isinstance(r, _Inequality) else (r, r.reversed) for r in Rel] |
|
|
|
results = [] |
|
|
|
for ((i, pi), (j, pj)) in combinations(enumerate(rtmp), 2): |
|
for pattern, simp in patterns: |
|
res = [] |
|
for p1, p2 in product(pi, pj): |
|
|
|
oldexpr = Tuple(p1, p2) |
|
tmpres = oldexpr.match(pattern) |
|
if tmpres: |
|
res.append((tmpres, oldexpr)) |
|
|
|
if res: |
|
for tmpres, oldexpr in res: |
|
|
|
np = simp.xreplace(tmpres) |
|
if np == dominatingvalue: |
|
|
|
|
|
return [replacementvalue] |
|
|
|
if not isinstance(np, ITE) and not np.has(Min, Max): |
|
|
|
|
|
costsaving = measure(func(*oldexpr.args)) - measure(np) |
|
if costsaving > 0: |
|
results.append((costsaving, ([i, j], np))) |
|
if results: |
|
|
|
results = sorted(results, |
|
key=lambda pair: pair[0], reverse=True) |
|
|
|
replacement = results[0][1] |
|
idx, newrel = replacement |
|
idx.sort() |
|
|
|
for index in reversed(idx): |
|
del Rel[index] |
|
if dominatingvalue is None or newrel != Not(dominatingvalue): |
|
|
|
|
|
if newrel.func == func: |
|
for a in newrel.args: |
|
Rel.append(a) |
|
else: |
|
Rel.append(newrel) |
|
|
|
changed = True |
|
return Rel |
|
|
|
|
|
def _apply_patternbased_threeterm_simplification(Rel, patterns, func, |
|
dominatingvalue, |
|
replacementvalue, |
|
measure): |
|
""" Apply pattern-based three-term simplification.""" |
|
from sympy.functions.elementary.miscellaneous import Min, Max |
|
from sympy.core.relational import Le, Lt, _Inequality |
|
changed = True |
|
while changed and len(Rel) >= 3: |
|
changed = False |
|
|
|
Rel = [r.reversed if isinstance(r, (Le, Lt)) else r for r in Rel] |
|
|
|
Rel = list(ordered(Rel)) |
|
|
|
results = [] |
|
|
|
rtmp = [(r, ) if isinstance(r, _Inequality) else (r, r.reversed) for r in Rel] |
|
|
|
for ((i, pi), (j, pj), (k, pk)) in permutations(enumerate(rtmp), 3): |
|
for pattern, simp in patterns: |
|
res = [] |
|
for p1, p2, p3 in product(pi, pj, pk): |
|
|
|
oldexpr = Tuple(p1, p2, p3) |
|
tmpres = oldexpr.match(pattern) |
|
if tmpres: |
|
res.append((tmpres, oldexpr)) |
|
|
|
if res: |
|
for tmpres, oldexpr in res: |
|
|
|
np = simp.xreplace(tmpres) |
|
if np == dominatingvalue: |
|
|
|
|
|
return [replacementvalue] |
|
|
|
if not isinstance(np, ITE) and not np.has(Min, Max): |
|
|
|
|
|
costsaving = measure(func(*oldexpr.args)) - measure(np) |
|
if costsaving > 0: |
|
results.append((costsaving, ([i, j, k], np))) |
|
if results: |
|
|
|
results = sorted(results, |
|
key=lambda pair: pair[0], reverse=True) |
|
|
|
replacement = results[0][1] |
|
idx, newrel = replacement |
|
idx.sort() |
|
|
|
for index in reversed(idx): |
|
del Rel[index] |
|
if dominatingvalue is None or newrel != Not(dominatingvalue): |
|
|
|
|
|
if newrel.func == func: |
|
for a in newrel.args: |
|
Rel.append(a) |
|
else: |
|
Rel.append(newrel) |
|
|
|
changed = True |
|
return Rel |
|
|
|
|
|
@cacheit |
|
def _simplify_patterns_and(): |
|
""" Two-term patterns for And.""" |
|
|
|
from sympy.core import Wild |
|
from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt |
|
from sympy.functions.elementary.complexes import Abs |
|
from sympy.functions.elementary.miscellaneous import Min, Max |
|
a = Wild('a') |
|
b = Wild('b') |
|
c = Wild('c') |
|
|
|
|
|
|
|
_matchers_and = ((Tuple(Eq(a, b), Lt(a, b)), false), |
|
|
|
|
|
|
|
(Tuple(Lt(b, a), Lt(a, b)), false), |
|
(Tuple(Eq(a, b), Le(b, a)), Eq(a, b)), |
|
|
|
|
|
(Tuple(Le(b, a), Le(a, b)), Eq(a, b)), |
|
|
|
|
|
(Tuple(Le(a, b), Lt(a, b)), Lt(a, b)), |
|
(Tuple(Le(a, b), Ne(a, b)), Lt(a, b)), |
|
(Tuple(Lt(a, b), Ne(a, b)), Lt(a, b)), |
|
|
|
(Tuple(Eq(a, b), Eq(a, -b)), And(Eq(a, S.Zero), Eq(b, S.Zero))), |
|
|
|
(Tuple(Le(b, a), Le(c, a)), Ge(a, Max(b, c))), |
|
(Tuple(Le(b, a), Lt(c, a)), ITE(b > c, Ge(a, b), Gt(a, c))), |
|
(Tuple(Lt(b, a), Lt(c, a)), Gt(a, Max(b, c))), |
|
(Tuple(Le(a, b), Le(a, c)), Le(a, Min(b, c))), |
|
(Tuple(Le(a, b), Lt(a, c)), ITE(b < c, Le(a, b), Lt(a, c))), |
|
(Tuple(Lt(a, b), Lt(a, c)), Lt(a, Min(b, c))), |
|
(Tuple(Le(a, b), Le(c, a)), ITE(Eq(b, c), Eq(a, b), ITE(b < c, false, And(Le(a, b), Ge(a, c))))), |
|
(Tuple(Le(c, a), Le(a, b)), ITE(Eq(b, c), Eq(a, b), ITE(b < c, false, And(Le(a, b), Ge(a, c))))), |
|
(Tuple(Lt(a, b), Lt(c, a)), ITE(b < c, false, And(Lt(a, b), Gt(a, c)))), |
|
(Tuple(Lt(c, a), Lt(a, b)), ITE(b < c, false, And(Lt(a, b), Gt(a, c)))), |
|
(Tuple(Le(a, b), Lt(c, a)), ITE(b <= c, false, And(Le(a, b), Gt(a, c)))), |
|
(Tuple(Le(c, a), Lt(a, b)), ITE(b <= c, false, And(Lt(a, b), Ge(a, c)))), |
|
(Tuple(Eq(a, b), Eq(a, c)), ITE(Eq(b, c), Eq(a, b), false)), |
|
(Tuple(Lt(a, b), Lt(-b, a)), ITE(b > 0, Lt(Abs(a), b), false)), |
|
(Tuple(Le(a, b), Le(-b, a)), ITE(b >= 0, Le(Abs(a), b), false)), |
|
) |
|
return _matchers_and |
|
|
|
|
|
@cacheit |
|
def _simplify_patterns_and3(): |
|
""" Three-term patterns for And.""" |
|
|
|
from sympy.core import Wild |
|
from sympy.core.relational import Eq, Ge, Gt |
|
|
|
a = Wild('a') |
|
b = Wild('b') |
|
c = Wild('c') |
|
|
|
|
|
|
|
_matchers_and = ((Tuple(Ge(a, b), Ge(b, c), Gt(c, a)), false), |
|
(Tuple(Ge(a, b), Gt(b, c), Gt(c, a)), false), |
|
(Tuple(Gt(a, b), Gt(b, c), Gt(c, a)), false), |
|
|
|
|
|
|
|
(Tuple(Ge(a, b), Ge(a, c), Ge(b, c)), And(Ge(a, b), Ge(b, c))), |
|
(Tuple(Ge(a, b), Ge(a, c), Gt(b, c)), And(Ge(a, b), Gt(b, c))), |
|
|
|
(Tuple(Ge(a, b), Gt(a, c), Gt(b, c)), And(Ge(a, b), Gt(b, c))), |
|
|
|
(Tuple(Ge(a, c), Gt(a, b), Gt(b, c)), And(Gt(a, b), Gt(b, c))), |
|
(Tuple(Ge(b, c), Gt(a, b), Gt(a, c)), And(Gt(a, b), Ge(b, c))), |
|
(Tuple(Gt(a, b), Gt(a, c), Gt(b, c)), And(Gt(a, b), Gt(b, c))), |
|
|
|
|
|
(Tuple(Ge(b, a), Ge(c, a), Ge(b, c)), And(Ge(c, a), Ge(b, c))), |
|
(Tuple(Ge(b, a), Ge(c, a), Gt(b, c)), And(Ge(c, a), Gt(b, c))), |
|
|
|
(Tuple(Ge(b, a), Gt(c, a), Gt(b, c)), And(Gt(c, a), Gt(b, c))), |
|
|
|
(Tuple(Ge(c, a), Gt(b, a), Gt(b, c)), And(Ge(c, a), Gt(b, c))), |
|
(Tuple(Ge(b, c), Gt(b, a), Gt(c, a)), And(Gt(c, a), Ge(b, c))), |
|
(Tuple(Gt(b, a), Gt(c, a), Gt(b, c)), And(Gt(c, a), Gt(b, c))), |
|
|
|
(Tuple(Ge(a, b), Ge(b, c), Ge(c, a)), And(Eq(a, b), Eq(b, c))), |
|
) |
|
return _matchers_and |
|
|
|
|
|
@cacheit |
|
def _simplify_patterns_or(): |
|
""" Two-term patterns for Or.""" |
|
|
|
from sympy.core import Wild |
|
from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt |
|
from sympy.functions.elementary.complexes import Abs |
|
from sympy.functions.elementary.miscellaneous import Min, Max |
|
a = Wild('a') |
|
b = Wild('b') |
|
c = Wild('c') |
|
|
|
|
|
|
|
_matchers_or = ((Tuple(Le(b, a), Le(a, b)), true), |
|
|
|
(Tuple(Le(b, a), Ne(a, b)), true), |
|
|
|
|
|
|
|
|
|
(Tuple(Eq(a, b), Le(a, b)), Le(a, b)), |
|
(Tuple(Eq(a, b), Lt(a, b)), Le(a, b)), |
|
|
|
(Tuple(Lt(b, a), Lt(a, b)), Ne(a, b)), |
|
(Tuple(Lt(b, a), Ne(a, b)), Ne(a, b)), |
|
(Tuple(Le(a, b), Lt(a, b)), Le(a, b)), |
|
|
|
(Tuple(Eq(a, b), Ne(a, c)), ITE(Eq(b, c), true, Ne(a, c))), |
|
(Tuple(Ne(a, b), Ne(a, c)), ITE(Eq(b, c), Ne(a, b), true)), |
|
|
|
(Tuple(Le(b, a), Le(c, a)), Ge(a, Min(b, c))), |
|
|
|
(Tuple(Le(b, a), Lt(c, a)), ITE(b > c, Lt(c, a), Le(b, a))), |
|
(Tuple(Lt(b, a), Lt(c, a)), Gt(a, Min(b, c))), |
|
|
|
(Tuple(Le(a, b), Le(a, c)), Le(a, Max(b, c))), |
|
|
|
(Tuple(Le(a, b), Lt(a, c)), ITE(b >= c, Le(a, b), Lt(a, c))), |
|
(Tuple(Lt(a, b), Lt(a, c)), Lt(a, Max(b, c))), |
|
|
|
(Tuple(Le(a, b), Le(c, a)), ITE(b >= c, true, Or(Le(a, b), Ge(a, c)))), |
|
(Tuple(Le(c, a), Le(a, b)), ITE(b >= c, true, Or(Le(a, b), Ge(a, c)))), |
|
(Tuple(Lt(a, b), Lt(c, a)), ITE(b > c, true, Or(Lt(a, b), Gt(a, c)))), |
|
(Tuple(Lt(c, a), Lt(a, b)), ITE(b > c, true, Or(Lt(a, b), Gt(a, c)))), |
|
(Tuple(Le(a, b), Lt(c, a)), ITE(b >= c, true, Or(Le(a, b), Gt(a, c)))), |
|
(Tuple(Le(c, a), Lt(a, b)), ITE(b >= c, true, Or(Lt(a, b), Ge(a, c)))), |
|
(Tuple(Lt(b, a), Lt(a, -b)), ITE(b >= 0, Gt(Abs(a), b), true)), |
|
(Tuple(Le(b, a), Le(a, -b)), ITE(b > 0, Ge(Abs(a), b), true)), |
|
) |
|
return _matchers_or |
|
|
|
|
|
@cacheit |
|
def _simplify_patterns_xor(): |
|
""" Two-term patterns for Xor.""" |
|
|
|
from sympy.functions.elementary.miscellaneous import Min, Max |
|
from sympy.core import Wild |
|
from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt |
|
a = Wild('a') |
|
b = Wild('b') |
|
c = Wild('c') |
|
|
|
|
|
|
|
_matchers_xor = ( |
|
|
|
|
|
|
|
(Tuple(Eq(a, b), Le(a, b)), Lt(a, b)), |
|
(Tuple(Eq(a, b), Lt(a, b)), Le(a, b)), |
|
(Tuple(Le(a, b), Lt(a, b)), Eq(a, b)), |
|
(Tuple(Le(a, b), Le(b, a)), Ne(a, b)), |
|
(Tuple(Le(b, a), Ne(a, b)), Le(a, b)), |
|
|
|
(Tuple(Lt(b, a), Ne(a, b)), Lt(a, b)), |
|
|
|
|
|
|
|
|
|
(Tuple(Le(b, a), Le(c, a)), |
|
And(Ge(a, Min(b, c)), Lt(a, Max(b, c)))), |
|
(Tuple(Le(b, a), Lt(c, a)), |
|
ITE(b > c, And(Gt(a, c), Lt(a, b)), |
|
And(Ge(a, b), Le(a, c)))), |
|
(Tuple(Lt(b, a), Lt(c, a)), |
|
And(Gt(a, Min(b, c)), Le(a, Max(b, c)))), |
|
(Tuple(Le(a, b), Le(a, c)), |
|
And(Le(a, Max(b, c)), Gt(a, Min(b, c)))), |
|
(Tuple(Le(a, b), Lt(a, c)), |
|
ITE(b < c, And(Lt(a, c), Gt(a, b)), |
|
And(Le(a, b), Ge(a, c)))), |
|
(Tuple(Lt(a, b), Lt(a, c)), |
|
And(Lt(a, Max(b, c)), Ge(a, Min(b, c)))), |
|
) |
|
return _matchers_xor |
|
|
|
|
|
def simplify_univariate(expr): |
|
"""return a simplified version of univariate boolean expression, else ``expr``""" |
|
from sympy.functions.elementary.piecewise import Piecewise |
|
from sympy.core.relational import Eq, Ne |
|
if not isinstance(expr, BooleanFunction): |
|
return expr |
|
if expr.atoms(Eq, Ne): |
|
return expr |
|
c = expr |
|
free = c.free_symbols |
|
if len(free) != 1: |
|
return c |
|
x = free.pop() |
|
ok, i = Piecewise((0, c), evaluate=False |
|
)._intervals(x, err_on_Eq=True) |
|
if not ok: |
|
return c |
|
if not i: |
|
return false |
|
args = [] |
|
for a, b, _, _ in i: |
|
if a is S.NegativeInfinity: |
|
if b is S.Infinity: |
|
c = true |
|
else: |
|
if c.subs(x, b) == True: |
|
c = (x <= b) |
|
else: |
|
c = (x < b) |
|
else: |
|
incl_a = (c.subs(x, a) == True) |
|
incl_b = (c.subs(x, b) == True) |
|
if incl_a and incl_b: |
|
if b.is_infinite: |
|
c = (x >= a) |
|
else: |
|
c = And(a <= x, x <= b) |
|
elif incl_a: |
|
c = And(a <= x, x < b) |
|
elif incl_b: |
|
if b.is_infinite: |
|
c = (x > a) |
|
else: |
|
c = And(a < x, x <= b) |
|
else: |
|
c = And(a < x, x < b) |
|
args.append(c) |
|
return Or(*args) |
|
|
|
|
|
|
|
|
|
BooleanGates = (And, Or, Xor, Nand, Nor, Not, Xnor, ITE) |
|
|
|
def gateinputcount(expr): |
|
""" |
|
Return the total number of inputs for the logic gates realizing the |
|
Boolean expression. |
|
|
|
Returns |
|
======= |
|
|
|
int |
|
Number of gate inputs |
|
|
|
Note |
|
==== |
|
|
|
Not all Boolean functions count as gate here, only those that are |
|
considered to be standard gates. These are: :py:class:`~.And`, |
|
:py:class:`~.Or`, :py:class:`~.Xor`, :py:class:`~.Not`, and |
|
:py:class:`~.ITE` (multiplexer). :py:class:`~.Nand`, :py:class:`~.Nor`, |
|
and :py:class:`~.Xnor` will be evaluated to ``Not(And())`` etc. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.logic import And, Or, Nand, Not, gateinputcount |
|
>>> from sympy.abc import x, y, z |
|
>>> expr = And(x, y) |
|
>>> gateinputcount(expr) |
|
2 |
|
>>> gateinputcount(Or(expr, z)) |
|
4 |
|
|
|
Note that ``Nand`` is automatically evaluated to ``Not(And())`` so |
|
|
|
>>> gateinputcount(Nand(x, y, z)) |
|
4 |
|
>>> gateinputcount(Not(And(x, y, z))) |
|
4 |
|
|
|
Although this can be avoided by using ``evaluate=False`` |
|
|
|
>>> gateinputcount(Nand(x, y, z, evaluate=False)) |
|
3 |
|
|
|
Also note that a comparison will count as a Boolean variable: |
|
|
|
>>> gateinputcount(And(x > z, y >= 2)) |
|
2 |
|
|
|
As will a symbol: |
|
>>> gateinputcount(x) |
|
0 |
|
|
|
""" |
|
if not isinstance(expr, Boolean): |
|
raise TypeError("Expression must be Boolean") |
|
if isinstance(expr, BooleanGates): |
|
return len(expr.args) + sum(gateinputcount(x) for x in expr.args) |
|
return 0 |
|
|