330 lines
8.3 KiB
Python
330 lines
8.3 KiB
Python
"""Inference in propositional logic"""
|
|
|
|
from sympy.logic.boolalg import And, Not, conjuncts, to_cnf, BooleanFunction
|
|
from sympy.core.sorting import ordered
|
|
from sympy.core.sympify import sympify
|
|
from sympy.external.importtools import import_module
|
|
|
|
|
|
def literal_symbol(literal):
|
|
"""
|
|
The symbol in this literal (without the negation).
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.abc import A
|
|
>>> from sympy.logic.inference import literal_symbol
|
|
>>> literal_symbol(A)
|
|
A
|
|
>>> literal_symbol(~A)
|
|
A
|
|
|
|
"""
|
|
|
|
if literal is True or literal is False:
|
|
return literal
|
|
try:
|
|
if literal.is_Symbol:
|
|
return literal
|
|
if literal.is_Not:
|
|
return literal_symbol(literal.args[0])
|
|
else:
|
|
raise ValueError
|
|
except (AttributeError, ValueError):
|
|
raise ValueError("Argument must be a boolean literal.")
|
|
|
|
|
|
def satisfiable(expr, algorithm=None, all_models=False, minimal=False):
|
|
"""
|
|
Check satisfiability of a propositional sentence.
|
|
Returns a model when it succeeds.
|
|
Returns {true: true} for trivially true expressions.
|
|
|
|
On setting all_models to True, if given expr is satisfiable then
|
|
returns a generator of models. However, if expr is unsatisfiable
|
|
then returns a generator containing the single element False.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.abc import A, B
|
|
>>> from sympy.logic.inference import satisfiable
|
|
>>> satisfiable(A & ~B)
|
|
{A: True, B: False}
|
|
>>> satisfiable(A & ~A)
|
|
False
|
|
>>> satisfiable(True)
|
|
{True: True}
|
|
>>> next(satisfiable(A & ~A, all_models=True))
|
|
False
|
|
>>> models = satisfiable((A >> B) & B, all_models=True)
|
|
>>> next(models)
|
|
{A: False, B: True}
|
|
>>> next(models)
|
|
{A: True, B: True}
|
|
>>> def use_models(models):
|
|
... for model in models:
|
|
... if model:
|
|
... # Do something with the model.
|
|
... print(model)
|
|
... else:
|
|
... # Given expr is unsatisfiable.
|
|
... print("UNSAT")
|
|
>>> use_models(satisfiable(A >> ~A, all_models=True))
|
|
{A: False}
|
|
>>> use_models(satisfiable(A ^ A, all_models=True))
|
|
UNSAT
|
|
|
|
"""
|
|
if algorithm is None or algorithm == "pycosat":
|
|
pycosat = import_module('pycosat')
|
|
if pycosat is not None:
|
|
algorithm = "pycosat"
|
|
else:
|
|
if algorithm == "pycosat":
|
|
raise ImportError("pycosat module is not present")
|
|
# Silently fall back to dpll2 if pycosat
|
|
# is not installed
|
|
algorithm = "dpll2"
|
|
|
|
if algorithm=="minisat22":
|
|
pysat = import_module('pysat')
|
|
if pysat is None:
|
|
algorithm = "dpll2"
|
|
|
|
if algorithm == "dpll":
|
|
from sympy.logic.algorithms.dpll import dpll_satisfiable
|
|
return dpll_satisfiable(expr)
|
|
elif algorithm == "dpll2":
|
|
from sympy.logic.algorithms.dpll2 import dpll_satisfiable
|
|
return dpll_satisfiable(expr, all_models)
|
|
elif algorithm == "pycosat":
|
|
from sympy.logic.algorithms.pycosat_wrapper import pycosat_satisfiable
|
|
return pycosat_satisfiable(expr, all_models)
|
|
elif algorithm == "minisat22":
|
|
from sympy.logic.algorithms.minisat22_wrapper import minisat22_satisfiable
|
|
return minisat22_satisfiable(expr, all_models, minimal)
|
|
raise NotImplementedError
|
|
|
|
|
|
def valid(expr):
|
|
"""
|
|
Check validity of a propositional sentence.
|
|
A valid propositional sentence is True under every assignment.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.abc import A, B
|
|
>>> from sympy.logic.inference import valid
|
|
>>> valid(A | ~A)
|
|
True
|
|
>>> valid(A | B)
|
|
False
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] https://en.wikipedia.org/wiki/Validity
|
|
|
|
"""
|
|
return not satisfiable(Not(expr))
|
|
|
|
|
|
def pl_true(expr, model=None, deep=False):
|
|
"""
|
|
Returns whether the given assignment is a model or not.
|
|
|
|
If the assignment does not specify the value for every proposition,
|
|
this may return None to indicate 'not obvious'.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
model : dict, optional, default: {}
|
|
Mapping of symbols to boolean values to indicate assignment.
|
|
deep: boolean, optional, default: False
|
|
Gives the value of the expression under partial assignments
|
|
correctly. May still return None to indicate 'not obvious'.
|
|
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.abc import A, B
|
|
>>> from sympy.logic.inference import pl_true
|
|
>>> pl_true( A & B, {A: True, B: True})
|
|
True
|
|
>>> pl_true(A & B, {A: False})
|
|
False
|
|
>>> pl_true(A & B, {A: True})
|
|
>>> pl_true(A & B, {A: True}, deep=True)
|
|
>>> pl_true(A >> (B >> A))
|
|
>>> pl_true(A >> (B >> A), deep=True)
|
|
True
|
|
>>> pl_true(A & ~A)
|
|
>>> pl_true(A & ~A, deep=True)
|
|
False
|
|
>>> pl_true(A & B & (~A | ~B), {A: True})
|
|
>>> pl_true(A & B & (~A | ~B), {A: True}, deep=True)
|
|
False
|
|
|
|
"""
|
|
|
|
from sympy.core.symbol import Symbol
|
|
|
|
boolean = (True, False)
|
|
|
|
def _validate(expr):
|
|
if isinstance(expr, Symbol) or expr in boolean:
|
|
return True
|
|
if not isinstance(expr, BooleanFunction):
|
|
return False
|
|
return all(_validate(arg) for arg in expr.args)
|
|
|
|
if expr in boolean:
|
|
return expr
|
|
expr = sympify(expr)
|
|
if not _validate(expr):
|
|
raise ValueError("%s is not a valid boolean expression" % expr)
|
|
if not model:
|
|
model = {}
|
|
model = {k: v for k, v in model.items() if v in boolean}
|
|
result = expr.subs(model)
|
|
if result in boolean:
|
|
return bool(result)
|
|
if deep:
|
|
model = {k: True for k in result.atoms()}
|
|
if pl_true(result, model):
|
|
if valid(result):
|
|
return True
|
|
else:
|
|
if not satisfiable(result):
|
|
return False
|
|
return None
|
|
|
|
|
|
def entails(expr, formula_set=None):
|
|
"""
|
|
Check whether the given expr_set entail an expr.
|
|
If formula_set is empty then it returns the validity of expr.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.abc import A, B, C
|
|
>>> from sympy.logic.inference import entails
|
|
>>> entails(A, [A >> B, B >> C])
|
|
False
|
|
>>> entails(C, [A >> B, B >> C, A])
|
|
True
|
|
>>> entails(A >> B)
|
|
False
|
|
>>> entails(A >> (B >> A))
|
|
True
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] https://en.wikipedia.org/wiki/Logical_consequence
|
|
|
|
"""
|
|
if formula_set:
|
|
formula_set = list(formula_set)
|
|
else:
|
|
formula_set = []
|
|
formula_set.append(Not(expr))
|
|
return not satisfiable(And(*formula_set))
|
|
|
|
|
|
class KB:
|
|
"""Base class for all knowledge bases"""
|
|
def __init__(self, sentence=None):
|
|
self.clauses_ = set()
|
|
if sentence:
|
|
self.tell(sentence)
|
|
|
|
def tell(self, sentence):
|
|
raise NotImplementedError
|
|
|
|
def ask(self, query):
|
|
raise NotImplementedError
|
|
|
|
def retract(self, sentence):
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def clauses(self):
|
|
return list(ordered(self.clauses_))
|
|
|
|
|
|
class PropKB(KB):
|
|
"""A KB for Propositional Logic. Inefficient, with no indexing."""
|
|
|
|
def tell(self, sentence):
|
|
"""Add the sentence's clauses to the KB
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.logic.inference import PropKB
|
|
>>> from sympy.abc import x, y
|
|
>>> l = PropKB()
|
|
>>> l.clauses
|
|
[]
|
|
|
|
>>> l.tell(x | y)
|
|
>>> l.clauses
|
|
[x | y]
|
|
|
|
>>> l.tell(y)
|
|
>>> l.clauses
|
|
[y, x | y]
|
|
|
|
"""
|
|
for c in conjuncts(to_cnf(sentence)):
|
|
self.clauses_.add(c)
|
|
|
|
def ask(self, query):
|
|
"""Checks if the query is true given the set of clauses.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.logic.inference import PropKB
|
|
>>> from sympy.abc import x, y
|
|
>>> l = PropKB()
|
|
>>> l.tell(x & ~y)
|
|
>>> l.ask(x)
|
|
True
|
|
>>> l.ask(y)
|
|
False
|
|
|
|
"""
|
|
return entails(query, self.clauses_)
|
|
|
|
def retract(self, sentence):
|
|
"""Remove the sentence's clauses from the KB
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.logic.inference import PropKB
|
|
>>> from sympy.abc import x, y
|
|
>>> l = PropKB()
|
|
>>> l.clauses
|
|
[]
|
|
|
|
>>> l.tell(x | y)
|
|
>>> l.clauses
|
|
[x | y]
|
|
|
|
>>> l.retract(x | y)
|
|
>>> l.clauses
|
|
[]
|
|
|
|
"""
|
|
for c in conjuncts(to_cnf(sentence)):
|
|
self.clauses_.discard(c)
|