You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

454 lines
12 KiB

5 months ago
"""
The classes used here are for the internal use of assumptions system
only and should not be used anywhere else as these do not possess the
signatures common to SymPy objects. For general use of logic constructs
please refer to sympy.logic classes And, Or, Not, etc.
"""
from itertools import combinations, product, zip_longest
from sympy.assumptions.assume import AppliedPredicate, Predicate
from sympy.core.relational import Eq, Ne, Gt, Lt, Ge, Le
from sympy.core.singleton import S
from sympy.logic.boolalg import Or, And, Not, Xnor
from sympy.logic.boolalg import (Equivalent, ITE, Implies, Nand, Nor, Xor)
class Literal:
"""
The smallest element of a CNF object.
Parameters
==========
lit : Boolean expression
is_Not : bool
Examples
========
>>> from sympy import Q
>>> from sympy.assumptions.cnf import Literal
>>> from sympy.abc import x
>>> Literal(Q.even(x))
Literal(Q.even(x), False)
>>> Literal(~Q.even(x))
Literal(Q.even(x), True)
"""
def __new__(cls, lit, is_Not=False):
if isinstance(lit, Not):
lit = lit.args[0]
is_Not = True
elif isinstance(lit, (AND, OR, Literal)):
return ~lit if is_Not else lit
obj = super().__new__(cls)
obj.lit = lit
obj.is_Not = is_Not
return obj
@property
def arg(self):
return self.lit
def rcall(self, expr):
if callable(self.lit):
lit = self.lit(expr)
else:
try:
lit = self.lit.apply(expr)
except AttributeError:
lit = self.lit.rcall(expr)
return type(self)(lit, self.is_Not)
def __invert__(self):
is_Not = not self.is_Not
return Literal(self.lit, is_Not)
def __str__(self):
return '{}({}, {})'.format(type(self).__name__, self.lit, self.is_Not)
__repr__ = __str__
def __eq__(self, other):
return self.arg == other.arg and self.is_Not == other.is_Not
def __hash__(self):
h = hash((type(self).__name__, self.arg, self.is_Not))
return h
class OR:
"""
A low-level implementation for Or
"""
def __init__(self, *args):
self._args = args
@property
def args(self):
return sorted(self._args, key=str)
def rcall(self, expr):
return type(self)(*[arg.rcall(expr)
for arg in self._args
])
def __invert__(self):
return AND(*[~arg for arg in self._args])
def __hash__(self):
return hash((type(self).__name__,) + tuple(self.args))
def __eq__(self, other):
return self.args == other.args
def __str__(self):
s = '(' + ' | '.join([str(arg) for arg in self.args]) + ')'
return s
__repr__ = __str__
class AND:
"""
A low-level implementation for And
"""
def __init__(self, *args):
self._args = args
def __invert__(self):
return OR(*[~arg for arg in self._args])
@property
def args(self):
return sorted(self._args, key=str)
def rcall(self, expr):
return type(self)(*[arg.rcall(expr)
for arg in self._args
])
def __hash__(self):
return hash((type(self).__name__,) + tuple(self.args))
def __eq__(self, other):
return self.args == other.args
def __str__(self):
s = '('+' & '.join([str(arg) for arg in self.args])+')'
return s
__repr__ = __str__
def to_NNF(expr, composite_map=None):
"""
Generates the Negation Normal Form of any boolean expression in terms
of AND, OR, and Literal objects.
Examples
========
>>> from sympy import Q, Eq
>>> from sympy.assumptions.cnf import to_NNF
>>> from sympy.abc import x, y
>>> expr = Q.even(x) & ~Q.positive(x)
>>> to_NNF(expr)
(Literal(Q.even(x), False) & Literal(Q.positive(x), True))
Supported boolean objects are converted to corresponding predicates.
>>> to_NNF(Eq(x, y))
Literal(Q.eq(x, y), False)
If ``composite_map`` argument is given, ``to_NNF`` decomposes the
specified predicate into a combination of primitive predicates.
>>> cmap = {Q.nonpositive: Q.negative | Q.zero}
>>> to_NNF(Q.nonpositive, cmap)
(Literal(Q.negative, False) | Literal(Q.zero, False))
>>> to_NNF(Q.nonpositive(x), cmap)
(Literal(Q.negative(x), False) | Literal(Q.zero(x), False))
"""
from sympy.assumptions.ask import Q
if composite_map is None:
composite_map = {}
binrelpreds = {Eq: Q.eq, Ne: Q.ne, Gt: Q.gt, Lt: Q.lt, Ge: Q.ge, Le: Q.le}
if type(expr) in binrelpreds:
pred = binrelpreds[type(expr)]
expr = pred(*expr.args)
if isinstance(expr, Not):
arg = expr.args[0]
tmp = to_NNF(arg, composite_map) # Strategy: negate the NNF of expr
return ~tmp
if isinstance(expr, Or):
return OR(*[to_NNF(x, composite_map) for x in Or.make_args(expr)])
if isinstance(expr, And):
return AND(*[to_NNF(x, composite_map) for x in And.make_args(expr)])
if isinstance(expr, Nand):
tmp = AND(*[to_NNF(x, composite_map) for x in expr.args])
return ~tmp
if isinstance(expr, Nor):
tmp = OR(*[to_NNF(x, composite_map) for x in expr.args])
return ~tmp
if isinstance(expr, Xor):
cnfs = []
for i in range(0, len(expr.args) + 1, 2):
for neg in combinations(expr.args, i):
clause = [~to_NNF(s, composite_map) if s in neg else to_NNF(s, composite_map)
for s in expr.args]
cnfs.append(OR(*clause))
return AND(*cnfs)
if isinstance(expr, Xnor):
cnfs = []
for i in range(0, len(expr.args) + 1, 2):
for neg in combinations(expr.args, i):
clause = [~to_NNF(s, composite_map) if s in neg else to_NNF(s, composite_map)
for s in expr.args]
cnfs.append(OR(*clause))
return ~AND(*cnfs)
if isinstance(expr, Implies):
L, R = to_NNF(expr.args[0], composite_map), to_NNF(expr.args[1], composite_map)
return OR(~L, R)
if isinstance(expr, Equivalent):
cnfs = []
for a, b in zip_longest(expr.args, expr.args[1:], fillvalue=expr.args[0]):
a = to_NNF(a, composite_map)
b = to_NNF(b, composite_map)
cnfs.append(OR(~a, b))
return AND(*cnfs)
if isinstance(expr, ITE):
L = to_NNF(expr.args[0], composite_map)
M = to_NNF(expr.args[1], composite_map)
R = to_NNF(expr.args[2], composite_map)
return AND(OR(~L, M), OR(L, R))
if isinstance(expr, AppliedPredicate):
pred, args = expr.function, expr.arguments
newpred = composite_map.get(pred, None)
if newpred is not None:
return to_NNF(newpred.rcall(*args), composite_map)
if isinstance(expr, Predicate):
newpred = composite_map.get(expr, None)
if newpred is not None:
return to_NNF(newpred, composite_map)
return Literal(expr)
def distribute_AND_over_OR(expr):
"""
Distributes AND over OR in the NNF expression.
Returns the result( Conjunctive Normal Form of expression)
as a CNF object.
"""
if not isinstance(expr, (AND, OR)):
tmp = set()
tmp.add(frozenset((expr,)))
return CNF(tmp)
if isinstance(expr, OR):
return CNF.all_or(*[distribute_AND_over_OR(arg)
for arg in expr._args])
if isinstance(expr, AND):
return CNF.all_and(*[distribute_AND_over_OR(arg)
for arg in expr._args])
class CNF:
"""
Class to represent CNF of a Boolean expression.
Consists of set of clauses, which themselves are stored as
frozenset of Literal objects.
Examples
========
>>> from sympy import Q
>>> from sympy.assumptions.cnf import CNF
>>> from sympy.abc import x
>>> cnf = CNF.from_prop(Q.real(x) & ~Q.zero(x))
>>> cnf.clauses
{frozenset({Literal(Q.zero(x), True)}),
frozenset({Literal(Q.negative(x), False),
Literal(Q.positive(x), False), Literal(Q.zero(x), False)})}
"""
def __init__(self, clauses=None):
if not clauses:
clauses = set()
self.clauses = clauses
def add(self, prop):
clauses = CNF.to_CNF(prop).clauses
self.add_clauses(clauses)
def __str__(self):
s = ' & '.join(
['(' + ' | '.join([str(lit) for lit in clause]) +')'
for clause in self.clauses]
)
return s
def extend(self, props):
for p in props:
self.add(p)
return self
def copy(self):
return CNF(set(self.clauses))
def add_clauses(self, clauses):
self.clauses |= clauses
@classmethod
def from_prop(cls, prop):
res = cls()
res.add(prop)
return res
def __iand__(self, other):
self.add_clauses(other.clauses)
return self
def all_predicates(self):
predicates = set()
for c in self.clauses:
predicates |= {arg.lit for arg in c}
return predicates
def _or(self, cnf):
clauses = set()
for a, b in product(self.clauses, cnf.clauses):
tmp = set(a)
for t in b:
tmp.add(t)
clauses.add(frozenset(tmp))
return CNF(clauses)
def _and(self, cnf):
clauses = self.clauses.union(cnf.clauses)
return CNF(clauses)
def _not(self):
clss = list(self.clauses)
ll = set()
for x in clss[-1]:
ll.add(frozenset((~x,)))
ll = CNF(ll)
for rest in clss[:-1]:
p = set()
for x in rest:
p.add(frozenset((~x,)))
ll = ll._or(CNF(p))
return ll
def rcall(self, expr):
clause_list = []
for clause in self.clauses:
lits = [arg.rcall(expr) for arg in clause]
clause_list.append(OR(*lits))
expr = AND(*clause_list)
return distribute_AND_over_OR(expr)
@classmethod
def all_or(cls, *cnfs):
b = cnfs[0].copy()
for rest in cnfs[1:]:
b = b._or(rest)
return b
@classmethod
def all_and(cls, *cnfs):
b = cnfs[0].copy()
for rest in cnfs[1:]:
b = b._and(rest)
return b
@classmethod
def to_CNF(cls, expr):
from sympy.assumptions.facts import get_composite_predicates
expr = to_NNF(expr, get_composite_predicates())
expr = distribute_AND_over_OR(expr)
return expr
@classmethod
def CNF_to_cnf(cls, cnf):
"""
Converts CNF object to SymPy's boolean expression
retaining the form of expression.
"""
def remove_literal(arg):
return Not(arg.lit) if arg.is_Not else arg.lit
return And(*(Or(*(remove_literal(arg) for arg in clause)) for clause in cnf.clauses))
class EncodedCNF:
"""
Class for encoding the CNF expression.
"""
def __init__(self, data=None, encoding=None):
if not data and not encoding:
data = []
encoding = {}
self.data = data
self.encoding = encoding
self._symbols = list(encoding.keys())
def from_cnf(self, cnf):
self._symbols = list(cnf.all_predicates())
n = len(self._symbols)
self.encoding = dict(zip(self._symbols, range(1, n + 1)))
self.data = [self.encode(clause) for clause in cnf.clauses]
@property
def symbols(self):
return self._symbols
@property
def variables(self):
return range(1, len(self._symbols) + 1)
def copy(self):
new_data = [set(clause) for clause in self.data]
return EncodedCNF(new_data, dict(self.encoding))
def add_prop(self, prop):
cnf = CNF.from_prop(prop)
self.add_from_cnf(cnf)
def add_from_cnf(self, cnf):
clauses = [self.encode(clause) for clause in cnf.clauses]
self.data += clauses
def encode_arg(self, arg):
literal = arg.lit
value = self.encoding.get(literal, None)
if value is None:
n = len(self._symbols)
self._symbols.append(literal)
value = self.encoding[literal] = n + 1
if arg.is_Not:
return -value
else:
return value
def encode(self, clause):
return {self.encode_arg(arg) if not arg.lit == S.false else 0 for arg in clause}