Source code for logic1.abc.bnf
"""This module :mod:`logic1.abc.bnf` provides a generic abstract implementation
of boolean normal form computations using the famous Espresso algorithm in
combination with boolean abstraction. Techincally, we use the python package
`PyEDA <https://pyeda.readthedocs.io/en/latest/index.html>`_, which in turns
wraps a `C extension
<https://ptolemy.berkeley.edu/projects/embedded/pubs/downloads/espresso/_index.htm>`_
of the famous Berkeley Espresso library [BraytonEtAl-1984]_.
"""
from abc import abstractmethod
from dataclasses import dataclass, field
from pyeda.boolalg import expr, minimization # type: ignore
from typing import ClassVar, Generic, TypeVar
from .. import firstorder
from ..firstorder import (
And, AtomicFormula, BooleanFormula, _F, Formula, Not, Or, _T, Term, Variable)
from ..support.tracing import trace # noqa
α = TypeVar('α', bound='AtomicFormula')
τ = TypeVar('τ', bound='Term')
χ = TypeVar('χ', bound='Variable')
σ = TypeVar('σ')
[docs]
@dataclass
class BooleanNormalForm(Generic[α, τ, χ, σ]):
"""Boolean normal form computation.
"""
_logic1_to_pyeda: ClassVar[dict[type[Formula], expr]] = {
firstorder.Equivalent: expr.Equal,
firstorder.Implies: expr.Implies,
firstorder.And: expr.And,
firstorder.Or: expr.Or,
firstorder.Not: expr.Not,
firstorder._T: expr._Zero,
firstorder._F: expr._One}
_index: int = 0
_atoms_to_pyeda: dict[AtomicFormula, expr.Literal] = field(default_factory=dict)
_pyeda_to_atoms: dict[expr.Literal, AtomicFormula] = field(default_factory=dict)
[docs]
def cnf(self, f: Formula[α, τ, χ, σ]) -> Formula[α, τ, χ, σ]:
"""Compute a conjunctive normal form. If `f` contains quantifiers, then
the result is a prenex normal form whose matrix is in CNF.
"""
return self.simplify(Not(self._dnf(Not(f))).to_nnf())
[docs]
def dnf(self, f: Formula[α, τ, χ, σ]) -> Formula[α, τ, χ, σ]:
"""Compute a disjunctive normal form. If `f` contains quantifiers, then
the result is a prenex normal form whose matrix is in DNF.
"""
return self.simplify(self._dnf(f))
def _dnf(self, f: Formula[α, τ, χ, σ]) -> Formula[α, τ, χ, σ]:
f = self.simplify(f.to_pnf())
mat, prefix = f.matrix()
match mat:
case And() | Or():
dnf: Formula = self._dnf_and_or(mat)
case AtomicFormula() | _F() | _T():
return f
case Not(arg=arg):
assert isinstance(arg, AtomicFormula)
return f
case _:
assert False
return dnf.quantify(prefix)
def _dnf_and_or(self, f: And[α, τ, χ, σ] | Or[α, τ, χ, σ]) \
-> AtomicFormula[α, τ, χ, σ] | BooleanFormula[α, τ, χ, σ]:
f_as_pyeda = self._to_pyeda(f)
dnf_as_pyeda = f_as_pyeda.to_dnf()
if not isinstance(dnf_as_pyeda, expr.Constant):
dnf_as_pyeda, = minimization.espresso_exprs(dnf_as_pyeda)
dnf = self._from_pyeda(dnf_as_pyeda)
return dnf
def _to_pyeda(self, f: AtomicFormula[α, τ, χ, σ] | And[α, τ, χ, σ] | Or[α, τ, χ, σ]) -> expr:
match f:
case AtomicFormula():
if f in self._atoms_to_pyeda:
return self._atoms_to_pyeda[f]
cf = f.to_complement()
if cf in self._atoms_to_pyeda:
return expr.Not(self._atoms_to_pyeda[cf])
new_exprvar = expr.exprvar('a', self._index)
self._index += 1
self._atoms_to_pyeda[f] = new_exprvar
self._pyeda_to_atoms[new_exprvar] = f
return new_exprvar
case And(args=args) | Or(args=args):
name = self._logic1_to_pyeda[f.op]
xs = (self._to_pyeda(arg) for arg in args)
return name(*xs, simplify=False)
case _:
assert False
def _from_pyeda(self, f: expr) -> AtomicFormula[α, τ, χ, σ] | BooleanFormula[α, τ, χ, σ]:
xs: expr
match f:
case expr.Variable():
return self._pyeda_to_atoms[f]
case expr.Complement():
# Complement of a variable is different from logical Not, and
# it is not covered by our dictionary
return self._pyeda_to_atoms[~ f].to_complement()
case expr.AndOp(xs=xs):
args = (self._from_pyeda(x) for x in xs)
return And(*args)
case expr.OrOp(xs=xs):
args = (self._from_pyeda(x) for x in xs)
return Or(*args)
case expr._Zero():
return _F()
case expr._One():
return _T()
case _:
assert False
[docs]
@abstractmethod
def simplify(self, f: Formula[α, τ, χ, σ]) -> Formula[α, τ, χ, σ]:
"""Compute a simplified equivalent of `f`.
"""
...