# Copyright 2016 Chris Drake
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
CFFI _boolexpr module wrapper
"""
# Ignore several instances of accessing obj._cdata
# pylint: disable=protected-access
# Some of the wrapper classes have only __dunder__ methods
# pylint: disable=too-few-public-methods
# This module needs to be a bit long
# pylint: disable=too-many-lines
import collections
import enum
import itertools
import operator
from functools import reduce
# pylint: disable=no-name-in-module
from . import ffi
from . import lib
# pylint: enable=no-name-in-module
from .util import clog2
class _String:
"""
Wrap C char *
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_String_del(self._cdata)
def __bytes__(self):
return ffi.string(self._cdata)
class _Vec:
"""
Wrap C Vec
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_Vec_del(self._cdata)
def __iter__(self):
lib.boolexpr_Vec_iter(self._cdata)
while True:
val = lib.boolexpr_Vec_val(self._cdata)
if val == ffi.NULL:
break
yield _bx(val)
lib.boolexpr_Vec_next(self._cdata)
class _VarSet:
"""
Wrap C VarSet
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_VarSet_del(self._cdata)
def __iter__(self):
lib.boolexpr_VarSet_iter(self._cdata)
while True:
val = lib.boolexpr_VarSet_val(self._cdata)
if val == ffi.NULL:
break
yield _bx(val)
lib.boolexpr_VarSet_next(self._cdata)
class _Point:
"""
Wrap C Point
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_Point_del(self._cdata)
def __iter__(self):
lib.boolexpr_Point_iter(self._cdata)
while True:
key = lib.boolexpr_Point_key(self._cdata)
if key == ffi.NULL:
break
val = lib.boolexpr_Point_val(self._cdata)
yield _bx(key), _bx(val)
lib.boolexpr_Point_next(self._cdata)
class _Soln:
"""
Wrap C Soln
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_Soln_del(self._cdata)
@property
def sat(self):
"""Return True if the solution is satisfiable."""
return bool(lib.boolexpr_Soln_first(self._cdata))
@property
def point(self):
"""Return a satisfying input point, or None."""
if not self.sat:
return None
return dict(_Point(lib.boolexpr_Soln_second(self._cdata)))
@property
def t(self):
"""Return the solution formatted as a tuple."""
return (self.sat, self.point)
class _DfsIter:
"""
Wrap C DfsIter
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_DfsIter_del(self._cdata)
def __iter__(self):
while True:
val = lib.boolexpr_DfsIter_val(self._cdata)
if val == ffi.NULL:
break
yield _bx(val)
lib.boolexpr_DfsIter_next(self._cdata)
class _SatIter:
"""
Wrap C SatIter
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_SatIter_del(self._cdata)
def __iter__(self):
while True:
val = lib.boolexpr_SatIter_val(self._cdata)
if val == ffi.NULL:
break
yield dict(_Point(val))
lib.boolexpr_SatIter_next(self._cdata)
class _PointsIter:
"""
Wrap C PointsIter
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_PointsIter_del(self._cdata)
def __iter__(self):
while True:
val = lib.boolexpr_PointsIter_val(self._cdata)
if val == ffi.NULL:
break
yield dict(_Point(val))
lib.boolexpr_PointsIter_next(self._cdata)
class _TermsIter:
"""
Wrap C TermsIter
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_TermsIter_del(self._cdata)
def __iter__(self):
while True:
val = lib.boolexpr_TermsIter_val(self._cdata)
if val == ffi.NULL:
break
yield tuple(_Vec(val))
lib.boolexpr_TermsIter_next(self._cdata)
class _DomainIter:
"""
Wrap C DomainIter
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_DomainIter_del(self._cdata)
def __iter__(self):
while True:
val = lib.boolexpr_DomainIter_val(self._cdata)
if val == ffi.NULL:
break
yield dict(_Point(val))
lib.boolexpr_DomainIter_next(self._cdata)
class _CofactorIter:
"""
Wrap C CofactorIter
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_CofactorIter_del(self._cdata)
def __iter__(self):
while True:
val = lib.boolexpr_CofactorIter_val(self._cdata)
if val == ffi.NULL:
break
yield _bx(val)
lib.boolexpr_CofactorIter_next(self._cdata)
class _ArrayPair:
"""
Wrap C ArrayPair
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_ArrayPair_del(self._cdata)
@property
def fst(self):
"""Return the first element of the pair."""
return Array(lib.boolexpr_ArrayPair_fst(self._cdata))
@property
def snd(self):
"""Return the second element of the pair."""
return Array(lib.boolexpr_ArrayPair_snd(self._cdata))
@property
def t(self):
"""Return the pair formatted as a tuple."""
return (self.fst, self.snd)
[docs]class Context:
"""
A context for Boolean variables
"""
def __init__(self):
self._cdata = lib.boolexpr_Context_new()
def __del__(self):
lib.boolexpr_Context_del(self._cdata)
[docs] def get_var(self, name):
"""Return a Variable with a given name."""
cdata = lib.boolexpr_Context_get_var(self._cdata, name.encode("ascii"))
return _bx(cdata)
[docs] def get_vars(self, name, *dims):
"""Return a multi-dimensional array of variables.
The *name* argument is a ``str``.
The variadic *dims* input is a sequence of dimension specs.
A dimension spec is a two-tuple: (start index, stop index).
If a dimension is given as a single ``int``,
it will be converted to ``(0, stop)``.
The dimension starts at index ``start``,
and increments by one up to, but not including, ``stop``.
This follows the Python slice convention.
"""
shape = _dims2shape(*dims)
objs = list()
for indices in itertools.product(*[range(i, j) for i, j in shape]):
suffix = "[" + ",".join(str(idx) for idx in indices) + "]"
objs.append(self.get_var(name + suffix))
return array(objs, shape)
ROOT_CONTEXT = Context()
def get_var(name):
"""Return a variable from the root context."""
return ROOT_CONTEXT.get_var(name)
def get_vars(name, *dims):
"""Return a multi-dimensional array of variables from the root context."""
return ROOT_CONTEXT.get_vars(name, *dims)
[docs]class BoolExpr:
"""
Wrap boolexpr::BoolExpr class
"""
[docs] class Kind(enum.Enum):
"""BoolExpr Kind Codes"""
zero = lib.ZERO
one = lib.ONE
log = lib.LOG
ill = lib.ILL
comp = lib.COMP
var = lib.VAR
nor = lib.NOR
or_ = lib.OR
nand = lib.NAND
and_ = lib.AND
xnor = lib.XNOR
xor = lib.XOR
neq = lib.NEQ
eq = lib.EQ
nimpl = lib.NIMPL
impl = lib.IMPL
nite = lib.NITE
ite = lib.ITE
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_BoolExpr_del(self._cdata)
[docs] def to_ast(self):
"""Convert to an abstract syntax tree (AST)."""
@classmethod
[docs] def from_ast(cls, ast):
"""Convert an abstract syntax tree (AST) to a BoolExpr."""
fst, rst = ast[0], ast[1:]
return _AST[fst](rst)
[docs] def to_dot(self):
"""Convert to DOT language representation."""
data = bytes(_String(lib.boolexpr_BoolExpr_to_dot(self._cdata)))
return data.decode("utf-8")
def __repr__(self):
return self.__str__()
def __bytes__(self):
return bytes(_String(lib.boolexpr_BoolExpr_to_string(self._cdata)))
def __str__(self):
return self.__bytes__().decode("utf-8")
[docs] def __invert__(self):
"""Boolean negation operator
+-----------+------------+
| :math:`f` | :math:`f'` |
+===========+============+
| 0 | 1 |
+-----------+------------+
| 1 | 0 |
+-----------+------------+
"""
return not_(self)
[docs] def __or__(self, other):
"""Boolean disjunction (sum, OR) operator
+-----------+-----------+---------------+
| :math:`f` | :math:`g` | :math:`f + g` |
+===========+===========+===============+
| 0 | 0 | 0 |
+-----------+-----------+---------------+
| 0 | 1 | 1 |
+-----------+-----------+---------------+
| 1 | 0 | 1 |
+-----------+-----------+---------------+
| 1 | 1 | 1 |
+-----------+-----------+---------------+
"""
return or_(self, other)
def __ror__(self, other):
return or_(other, self)
[docs] def __and__(self, other):
r"""Boolean conjunction (product, AND) operator
+-----------+-----------+-------------------+
| :math:`f` | :math:`g` | :math:`f \cdot g` |
+===========+===========+===================+
| 0 | 0 | 0 |
+-----------+-----------+-------------------+
| 0 | 1 | 0 |
+-----------+-----------+-------------------+
| 1 | 0 | 0 |
+-----------+-----------+-------------------+
| 1 | 1 | 1 |
+-----------+-----------+-------------------+
"""
return and_(self, other)
def __rand__(self, other):
return and_(other, self)
[docs] def __xor__(self, other):
r"""Boolean exclusive or (XOR) operator
+-----------+-----------+--------------------+
| :math:`f` | :math:`g` | :math:`f \oplus g` |
+===========+===========+====================+
| 0 | 0 | 0 |
+-----------+-----------+--------------------+
| 0 | 1 | 1 |
+-----------+-----------+--------------------+
| 1 | 0 | 1 |
+-----------+-----------+--------------------+
| 1 | 1 | 0 |
+-----------+-----------+--------------------+
"""
return xor(self, other)
def __rxor__(self, other):
return xor(other, self)
def __add__(self, other):
return array([self]) + other
def __radd__(self, other):
return array([other]) + self
def __mul__(self, num):
return array([self]) * num
def __rmul__(self, num):
return self.__mul__(num)
@property
def kind(self):
"""Return the Kind code.
The expression "kind" is one byte of information.
See ``include/boolexpr/boolexpr.h`` for the full table of kinds codes.
For convenience, this value is wrapped by a Python Enum.
"""
return self.Kind(lib.boolexpr_BoolExpr_kind(self._cdata))
[docs] def depth(self):
"""Return the depth of the expression.
1. An ``Atom`` has zero depth.
2. An ``Operator`` has depth equal to the maximum depth of its
arguments plus one.
"""
return lib.boolexpr_BoolExpr_depth(self._cdata)
[docs] def size(self):
"""Return the size of the expression.
1. An ``Atom`` has size one.
2. An ``Operator`` has size equal to the sum of its arguments' sizes
plus one.
"""
return lib.boolexpr_BoolExpr_size(self._cdata)
[docs] def is_cnf(self):
"""Return ``True`` if the expression is in conjunctive normal form (CNF).
A CNF is defined as a conjunction of clauses,
where a clause is defined as either a ``Literal``,
or a disjunction of literals.
Boolean True (``And`` identity) is also considered to be a CNF.
"""
return bool(lib.boolexpr_BoolExpr_is_cnf(self._cdata))
[docs] def is_dnf(self):
"""
Return ``True`` if the expression is in disjunctive normal form (DNF).
A DNF is defined as a disjunction of clauses,
where a clause is defined as either a ``Literal``,
or a disjunction of literals.
Boolean False (``Or`` identity) is also considered to be a DNF.
"""
return bool(lib.boolexpr_BoolExpr_is_dnf(self._cdata))
[docs] def simplify(self):
"""Return a simplified expression.
Simplification uses Boolean algebra identities to reduce the size
of the expression.
"""
return _bx(lib.boolexpr_BoolExpr_simplify(self._cdata))
[docs] def to_binop(self):
"""Return an expression that uses only binary operators."""
return _bx(lib.boolexpr_BoolExpr_to_binop(self._cdata))
[docs] def to_latop(self):
"""
Convert all operators to ``Or`` / ``And`` (lattice operator) form.
"""
return _bx(lib.boolexpr_BoolExpr_to_latop(self._cdata))
[docs] def to_posop(self):
r"""Return an expression with NOT bubbles pushed down through dual ops.
Specifically, perform the following transformations:
.. math::
\overline{a + b + \ldots} &\iff \overline{a\vphantom{b}} \cdot \overline{b} \cdot \ldots
\overline{a \cdot b \cdot \ldots} &\iff \overline{a\vphantom{b}} + \overline{b} + \ldots
\overline{a \oplus b \oplus \ldots} &\iff \overline{a} \oplus b \oplus \ldots
\overline{eq(a, b, \ldots)} &\iff eq(\overline{a}, b, \ldots)
\overline{p \implies q} &\iff p \cdot \overline{q}
\overline{ite(s, d1, d0)} &\iff ite(s, \overline{d1}, \overline{d0})
"""
return _bx(lib.boolexpr_BoolExpr_to_posop(self._cdata))
[docs] def tseytin(self, ctx, auxvarname="a"):
"""Return the Tseytin transformation.
The ``ctx`` parameter is a ``Context`` object that will be used to
store auxiliary variables.
The ``auxvarname`` parameter is the prefix of auxiliary variable names.
The suffix will be in the form ``_0``, ``_1``, etc.
"""
name = auxvarname.encode("ascii")
return _bx(lib.boolexpr_BoolExpr_tseytin(self._cdata, ctx._cdata, name))
[docs] def compose(self, var2bx):
r"""
Substitute a subset of support variables with other Boolean expressions.
Returns a new expression: :math:`f(g_i, \ldots)`
:math:`f_1 \: | \: x_i = f_2`
"""
num = len(var2bx)
c_vars = ffi.new("void * []", num)
c_bxs = ffi.new("void * []", num)
for i, (var, bx) in enumerate(var2bx.items()):
c_vars[i] = _expect_var(var)._cdata
c_bxs[i] = _expect_bx(bx)._cdata
return _bx(lib.boolexpr_BoolExpr_compose(self._cdata, num, c_vars, c_bxs))
[docs] def restrict(self, point):
r"""
Restrict a subset of support variables to :math:`\{0, 1\}`.
Returns a new function: :math:`f(x_i, \ldots)`
:math:`f \: | \: x_i = b`
"""
num = len(point)
c_vars = ffi.new("void * []", num)
c_consts = ffi.new("void * []", num)
for i, (var, const) in enumerate(point.items()):
c_vars[i] = _expect_var(var)._cdata
c_consts[i] = _expect_const(const)._cdata
return _bx(lib.boolexpr_BoolExpr_restrict(self._cdata, num, c_vars, c_consts))
[docs] def sat(self):
"""Return a tuple (sat, point).
The sat value is ``True`` if the expression is satisfiable.
If the expression is not satisfiable, the point will be ``None``.
Otherwise, it will return a satisfying input point.
"""
return _Soln(lib.boolexpr_BoolExpr_sat(self._cdata)).t
[docs] def iter_sat(self):
"""Iterate through all satisfying input points."""
yield from _SatIter(lib.boolexpr_SatIter_new(self._cdata))
[docs] def to_cnf(self):
"""Convert the expression to conjunctive normal form (CNF)."""
return _bx(lib.boolexpr_BoolExpr_to_cnf(self._cdata))
[docs] def to_dnf(self):
"""Convert the expression to disjunctive normal form (DNF)."""
return _bx(lib.boolexpr_BoolExpr_to_dnf(self._cdata))
[docs] def to_nnf(self):
"""Convert the expression to negation normal form (NNF).
Negation formal form is defined as:
* Only ``Or`` or ``And`` operators
* All NOT bubbles are pushed down to the literals
* Simple
"""
return _bx(lib.boolexpr_BoolExpr_to_nnf(self._cdata))
[docs] def equiv(self, other):
"""Return True if the two expressions are formally equivalent.
.. note:: While in practice this check can be quite fast,
SAT is an NP-complete problem, so some inputs will require
exponential runtime.
"""
other = _expect_bx(other)
return bool(lib.boolexpr_BoolExpr_equiv(self._cdata, other._cdata))
[docs] def support(self):
"""Return the support set of the expression."""
return set(_VarSet(lib.boolexpr_BoolExpr_support(self._cdata)))
[docs] def degree(self):
r"""Return the degree of a function.
A function from :math:`B^{N} \Rightarrow B` is called a Boolean
function of *degree* :math:`N`.
"""
return lib.boolexpr_BoolExpr_degree(self._cdata)
[docs] def expand(self, xs):
"""
Return the Shannon expansion with respect to a sequence of variables.
"""
if isinstance(xs, Variable):
xs = [xs]
num = len(xs)
c_vars = ffi.new("void * []", num)
for i, x in enumerate(xs):
c_vars[i] = _expect_var(x)._cdata
return _bx(lib.boolexpr_BoolExpr_expand(self._cdata, num, c_vars))
[docs] def smoothing(self, xs):
r"""Return the smoothing over a sequence of N variables.
The *xs* argument is a sequence of :math:`N` Boolean variables.
The *smoothing* of :math:`f(x_1, x_2, \dots, x_i, \dots, x_n)` with
respect to variable :math:`x_i` is:
:math:`S_{x_i}(f) = f_{x_i} + f_{x_i'}`
This is the same as the existential quantification operator:
:math:`\exists \{x_1, x_2, \dots\} \: f`
"""
if isinstance(xs, Variable):
xs = [xs]
num = len(xs)
c_vars = ffi.new("void * []", num)
for i, x in enumerate(xs):
c_vars[i] = _expect_var(x)._cdata
return _bx(lib.boolexpr_BoolExpr_smoothing(self._cdata, num, c_vars))
[docs] def consensus(self, xs):
r"""Return the consensus over a sequence of N variables.
The *xs* argument is a sequence of :math:`N` Boolean variables.
The *consensus* of :math:`f(x_1, x_2, \dots, x_i, \dots, x_n)` with
respect to variable :math:`x_i` is:
:math:`C_{x_i}(f) = f_{x_i} \cdot f_{x_i'}`
This is the same as the universal quantification operator:
:math:`\forall \{x_1, x_2, \dots\} \: f`
"""
if isinstance(xs, Variable):
xs = [xs]
num = len(xs)
c_vars = ffi.new("void * []", num)
for i, x in enumerate(xs):
c_vars[i] = _expect_var(x)._cdata
return _bx(lib.boolexpr_BoolExpr_consensus(self._cdata, num, c_vars))
[docs] def derivative(self, xs):
r"""Return the derivative over a sequence of N variables.
The *xs* argument is a sequence of :math:`N` Boolean variables.
The *derivative* of :math:`f(x_1, x_2, \dots, x_i, \dots, x_n)` with
respect to variable :math:`x_i` is:
:math:`\frac{\partial}{\partial x_i} f = f_{x_i} \oplus f_{x_i'}`
This is also known as the Boolean *difference*.
"""
if isinstance(xs, Variable):
xs = [xs]
num = len(xs)
c_vars = ffi.new("void * []", num)
for i, x in enumerate(xs):
c_vars[i] = _expect_var(x)._cdata
return _bx(lib.boolexpr_BoolExpr_derivative(self._cdata, num, c_vars))
[docs] def iter_dfs(self):
"""Iterate through all expression nodes in DFS order."""
yield from _DfsIter(lib.boolexpr_DfsIter_new(self._cdata))
[docs] def iter_domain(self):
"""Iterate through all points in the domain."""
yield from _DomainIter(lib.boolexpr_DomainIter_new(self._cdata))
[docs] def iter_cfs(self, xs):
"""Iterate through all cofactors over a sequence of variables."""
if isinstance(xs, Variable):
xs = [xs]
num = len(xs)
c_vars = ffi.new("void * []", num)
for i, x in enumerate(xs):
c_vars[i] = _expect_var(x)._cdata
cdata = lib.boolexpr_CofactorIter_new(self._cdata, num, c_vars)
yield from _CofactorIter(cdata)
[docs]class Atom(BoolExpr):
"""Boolean Atom"""
[docs]class Constant(Atom):
"""Boolean Constant Atom"""
[docs]class Known(Constant):
"""Boolean Known Constant"""
[docs]class Zero(Known):
"""Boolean Zero"""
def __bool__(self):
return False
def __int__(self):
return 0
def to_ast(self):
return (self.kind, )
[docs]class One(Known):
"""Boolean One"""
def __bool__(self):
return True
def __int__(self):
return 1
def to_ast(self):
return (self.kind, )
[docs]class Unknown(Constant):
"""Boolean Unknown Constant"""
[docs]class Logical(Unknown):
"""Boolean Logical Unknown"""
def to_ast(self):
return (self.kind, )
class Illogical(Unknown):
"""Boolean Illogical Unknown"""
def to_ast(self):
return (self.kind, )
[docs]class Literal(Atom):
"""Boolean Literal Atom"""
[docs] def __abs__(self):
"""Absolute value operator"""
return _bx(lib.boolexpr_abs(self._cdata))
[docs]class Complement(Literal):
"""Boolean Complement"""
def to_ast(self):
return (
self.kind,
int(ffi.cast("uintptr_t", lib.boolexpr_Literal_ctx(self._cdata))),
str(~self),
)
[docs]class Variable(Literal):
"""Boolean Variable"""
def to_ast(self):
return (
self.kind,
int(ffi.cast("uintptr_t", lib.boolexpr_Literal_ctx(self._cdata))),
self.__str__()
)
[docs]class Operator(BoolExpr):
"""Boolean Operator"""
def to_ast(self):
return (self.kind, ) + tuple(arg.to_ast() for arg in self.args)
@property
def simple(self):
"""Return ``True`` if the operator is simple.
An operator is only deemed simple if it has been returned by the
``simplify`` method.
"""
return bool(lib.boolexpr_Operator_simple(self._cdata))
@property
def args(self):
"""Return a tuple of the operator's arguments."""
return tuple(_Vec(lib.boolexpr_Operator_args(self._cdata)))
[docs] def is_clause(self):
"""Return ``True`` if the operator is a clause.
A *clause* is defined as having only ``Literal`` arguments.
"""
return bool(lib.boolexpr_Operator_is_clause(self._cdata))
[docs]class NegativeOperator(Operator):
"""Negative Operator"""
[docs]class LatticeOperator(Operator):
"""Boolean Lattice Operator"""
[docs]class Nor(NegativeOperator):
"""Boolean Nor Operator"""
[docs]class Nand(NegativeOperator):
"""Boolean Nand Operator"""
[docs]class Xnor(NegativeOperator):
"""Boolean Xnor Operator"""
[docs]class Unequal(NegativeOperator):
"""Boolean Unequal Operator"""
[docs]class NotImplies(NegativeOperator):
"""Boolean Not Implies Operator"""
[docs]class NotIfThenElse(NegativeOperator):
"""Boolean Not IfThenElse Operator"""
[docs]class Or(LatticeOperator):
"""Boolean Or Operator"""
[docs]class And(LatticeOperator):
"""Boolean And Operator"""
[docs]class Xor(Operator):
"""Boolean Xor Operator"""
[docs]class Equal(Operator):
"""Boolean Equal Operator"""
[docs]class Implies(Operator):
"""Boolean Implies Operator"""
[docs]class IfThenElse(Operator):
"""Boolean IfThenElse Operator"""
ZERO = Zero(lib.boolexpr_zero())
ONE = One(lib.boolexpr_one())
LOGICAL = Logical(lib.boolexpr_logical())
ILLOGICAL = Illogical(lib.boolexpr_illogical())
def iter_points(xs):
"""Iterate through all points in a Boolean space."""
if isinstance(xs, Variable):
xs = [xs]
num = len(xs)
c_vars = ffi.new("void * []", num)
for i, x in enumerate(xs):
c_vars[i] = _expect_var(x)._cdata
yield from _PointsIter(lib.boolexpr_PointsIter_new(len(c_vars), c_vars))
def iter_terms(*args):
"""Iterate through all terms in a Boolean space."""
_, c_bxs = _convert_args(args)
yield from _TermsIter(lib.boolexpr_TermsIter_new(len(c_bxs), c_bxs))
[docs]def not_(arg):
"""Boolean Not operator."""
_, c_bxs = _convert_args((arg, ))
return _bx(lib.boolexpr_not(c_bxs[0]))
[docs]def nor(*args):
"""Boolean Nor operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_nor(num, c_bxs))
[docs]def or_(*args):
"""Boolean Or operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_or(num, c_bxs))
[docs]def nand(*args):
"""Boolean Nand operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_nand(num, c_bxs))
[docs]def and_(*args):
"""Boolean And operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_and(num, c_bxs))
[docs]def xnor(*args):
"""Boolean Xnor operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_xnor(num, c_bxs))
[docs]def xor(*args):
"""Boolean Xor operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_xor(num, c_bxs))
[docs]def neq(*args):
"""Boolean Unequal operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_neq(num, c_bxs))
[docs]def eq(*args):
"""Boolean Equal operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_eq(num, c_bxs))
def nimpl(p, q):
"""Boolean NotImplies operator."""
_, c_bxs = _convert_args((p, q))
return _bx(lib.boolexpr_nimpl(c_bxs[0], c_bxs[1]))
[docs]def impl(p, q):
"""Boolean Implies operator."""
_, c_bxs = _convert_args((p, q))
return _bx(lib.boolexpr_impl(c_bxs[0], c_bxs[1]))
def nite(s, d1, d0):
"""Boolean NotIfThenElse operator."""
_, c_bxs = _convert_args((s, d1, d0))
return _bx(lib.boolexpr_nite(c_bxs[0], c_bxs[1], c_bxs[2]))
[docs]def ite(s, d1, d0):
"""Boolean IfThenElse operator."""
_, c_bxs = _convert_args((s, d1, d0))
return _bx(lib.boolexpr_ite(c_bxs[0], c_bxs[1], c_bxs[2]))
[docs]def nor_s(*args):
"""Simplifying Boolean Nor operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_nor_s(num, c_bxs))
[docs]def or_s(*args):
"""Simplifying Boolean Or operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_or_s(num, c_bxs))
[docs]def nand_s(*args):
"""Simplifying Boolean Nand operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_nand_s(num, c_bxs))
[docs]def and_s(*args):
"""Simplifying Boolean And operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_and_s(num, c_bxs))
[docs]def xnor_s(*args):
"""Simplifying Boolean Xnor operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_xnor_s(num, c_bxs))
[docs]def xor_s(*args):
"""Simplifying Boolean Xor operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_xor_s(num, c_bxs))
[docs]def neq_s(*args):
"""Simplifying Boolean Unequal operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_neq_s(num, c_bxs))
[docs]def eq_s(*args):
"""Simplifying Boolean Equal operator."""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_eq_s(num, c_bxs))
def nimpl_s(p, q):
"""Simplifying Boolean Implies operator."""
_, c_bxs = _convert_args((p, q))
return _bx(lib.boolexpr_nimpl_s(c_bxs[0], c_bxs[1]))
[docs]def impl_s(p, q):
"""Simplifying Boolean Implies operator."""
_, c_bxs = _convert_args((p, q))
return _bx(lib.boolexpr_impl_s(c_bxs[0], c_bxs[1]))
def nite_s(s, d1, d0):
"""Simplifying Boolean IfThenElse operator."""
_, c_bxs = _convert_args((s, d1, d0))
return _bx(lib.boolexpr_nite_s(c_bxs[0], c_bxs[1], c_bxs[2]))
[docs]def ite_s(s, d1, d0):
"""Simplifying Boolean IfThenElse operator."""
_, c_bxs = _convert_args((s, d1, d0))
return _bx(lib.boolexpr_ite_s(c_bxs[0], c_bxs[1], c_bxs[2]))
[docs]def onehot0(*args):
"""
Return an expression that means
"at most one input function is true".
"""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_onehot0(num, c_bxs))
[docs]def onehot(*args):
"""
Return an expression that means
"exactly one input function is true".
"""
num, c_bxs = _convert_args(args)
return _bx(lib.boolexpr_onehot(num, c_bxs))
_LITS = dict()
_KIND2CONST = {
lib.ZERO : ZERO,
lib.ONE : ONE,
lib.LOG : LOGICAL,
lib.ILL : ILLOGICAL,
}
_KIND2LIT = {
lib.COMP : Complement,
lib.VAR : Variable,
}
_KIND2OP = {
lib.NOR : Nor,
lib.OR : Or,
lib.NAND : Nand,
lib.AND : And,
lib.XNOR : Xnor,
lib.XOR : Xor,
lib.NEQ : Unequal,
lib.EQ : Equal,
lib.NIMPL : NotImplies,
lib.IMPL : Implies,
lib.NITE : NotIfThenElse,
lib.ITE : IfThenElse,
}
def _var(ctx_num, name):
ctx = ffi.cast("void *", ctx_num)
return _bx(lib.boolexpr_Context_get_var(ctx, name.encode("ascii")))
_AST = {
BoolExpr.Kind.zero : lambda _: ZERO,
BoolExpr.Kind.one : lambda _: ONE,
BoolExpr.Kind.log : lambda _: LOGICAL,
BoolExpr.Kind.zero : lambda _: ILLOGICAL,
BoolExpr.Kind.comp : lambda args: ~_var(*args),
BoolExpr.Kind.var : lambda args: _var(*args),
BoolExpr.Kind.nor : lambda asts: nor(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.or_ : lambda asts: or_(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.nand : lambda asts: nand(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.and_ : lambda asts: and_(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.xnor : lambda asts: xnor(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.xor : lambda asts: xor(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.neq : lambda asts: neq(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.eq : lambda asts: eq(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.nimpl : lambda asts: nimpl(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.impl : lambda asts: impl(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.nite : lambda asts: nite(*map(BoolExpr.from_ast, asts)),
BoolExpr.Kind.ite : lambda asts: ite(*map(BoolExpr.from_ast, asts)),
}
def _expect_array(obj):
"""Return an ndarray, or raise TypeError."""
if obj == 0 or obj == "0":
return array([ZERO])
elif obj == 1 or obj == "1":
return array([ONE])
elif obj == "x" or obj == "X":
return array([LOGICAL])
elif obj == "?":
return array([ILLOGICAL])
elif isinstance(obj, BoolExpr):
return array([obj])
elif isinstance(obj, ndarray):
return obj
else:
raise TypeError("Expected obj to be an ndarray")
def _expect_bx(obj):
"""Return a BoolExpr, or raise TypeError."""
if obj == 0 or obj == "0":
return ZERO
elif obj == 1 or obj == "1":
return ONE
elif obj == "x" or obj == "X":
return LOGICAL
elif obj == "?":
return ILLOGICAL
elif isinstance(obj, BoolExpr):
return obj
else:
raise TypeError("Expected obj to be a BoolExpr")
def _expect_const(obj):
"""Return a Constant, or raise TypeError."""
if obj == 0 or obj == "0":
return ZERO
elif obj == 1 or obj == "1":
return ONE
elif obj == "x" or obj == "X":
return LOGICAL
elif obj == "?":
return ILLOGICAL
elif isinstance(obj, Constant):
return obj
else:
raise TypeError("Expected obj to be a Constant")
def _expect_var(obj):
"""Return a Variable, or raise TypeError."""
if isinstance(obj, Variable):
return obj
else:
raise TypeError("Expected obj to be a Variable")
def _convert_args(args):
"""Convert a sequence of Python BoolExpr to a C [BoolExpr]."""
num = len(args)
c_args = ffi.new("void const * []", num)
for i, arg in enumerate(args):
c_args[i] = _expect_bx(arg)._cdata
return num, c_args
def _bx(cbx):
kind = lib.boolexpr_BoolExpr_kind(cbx)
if kind in _KIND2CONST:
lib.boolexpr_BoolExpr_del(cbx)
return _KIND2CONST[kind]
if kind in _KIND2LIT:
key = (int(ffi.cast("uintptr_t", lib.boolexpr_Literal_ctx(cbx))),
lib.boolexpr_Literal_id(cbx))
try:
lit = _LITS[key]
except KeyError:
lit = _LITS[key] = _KIND2LIT[kind](cbx)
else:
lib.boolexpr_BoolExpr_del(cbx)
return lit
return _KIND2OP[kind](cbx)
[docs]class Array:
"""
Wrap boolexpr::Array class
"""
def __init__(self, cdata):
self._cdata = cdata
def __del__(self):
lib.boolexpr_Array_del(self._cdata)
def __repr__(self):
return self.__str__()
def __str__(self):
return "(" + ", ".join(str(item) for item in self) + ")"
def __len__(self):
return lib.boolexpr_Array_size(self._cdata)
def __getitem__(self, key):
if isinstance(key, slice):
start, stop, _ = self._key2indices(key)
cdata = lib.boolexpr_Array_getslice(self._cdata, start, stop)
return Array(cdata)
index = self._key2index(key)
return _bx(lib.boolexpr_Array_getitem(self._cdata, index))
def __setitem__(self, key, val):
index = self._key2index(key)
lib.boolexpr_Array_setitem(self._cdata, index, val._cdata)
def _key2index(self, key):
index = operator.index(key)
if index < 0:
index += self.__len__()
if not 0 <= index < self.__len__():
raise IndexError("Array index out of range")
return index
def _key2indices(self, key):
if key.step is not None:
raise ValueError("Array slice step is not supported")
return key.indices(self.__len__())
def __iter__(self):
for i in range(self.__len__()):
yield self.__getitem__(i)
def __invert__(self):
return Array(lib.boolexpr_Array_invert(self._cdata))
def __or__(self, other):
return Array(lib.boolexpr_Array_or(self._cdata, other._cdata))
def __and__(self, other):
return Array(lib.boolexpr_Array_and(self._cdata, other._cdata))
def __xor__(self, other):
return Array(lib.boolexpr_Array_xor(self._cdata, other._cdata))
def __add__(self, other):
return Array(lib.boolexpr_Array_plus(self._cdata, other._cdata))
def __mul__(self, num):
return Array(lib.boolexpr_Array_mul(self._cdata, num))
def __rmul__(self, num):
return Array(lib.boolexpr_Array_mul(self._cdata, num))
def simplify(self):
"""Return a simplified array.
Simplification uses Boolean algebra identities to reduce the size
of the expression.
"""
return Array(lib.boolexpr_Array_simplify(self._cdata))
def compose(self, var2bx):
r"""
Substitute a subset of support variables with other Boolean expressions.
Returns a new expression: :math:`f(g_i, \ldots)`
:math:`f_1 \: | \: x_i = f_2`
"""
num = len(var2bx)
c_vars = ffi.new("void * []", num)
c_bxs = ffi.new("void * []", num)
for i, (var, bx) in enumerate(var2bx.items()):
c_vars[i] = _expect_var(var)._cdata
c_bxs[i] = _expect_bx(bx)._cdata
return Array(lib.boolexpr_Array_compose(self._cdata, num, c_vars, c_bxs))
def restrict(self, point):
r"""
Substitute a subset of support variables with other Boolean expressions.
Returns a new expression: :math:`f(g_i, \ldots)`
:math:`f_1 \: | \: x_i = f_2`
"""
num = len(point)
c_vars = ffi.new("void * []", num)
c_consts = ffi.new("void * []", num)
for i, (var, const) in enumerate(point.items()):
c_vars[i] = _expect_var(var)._cdata
c_consts[i] = _expect_const(const)._cdata
return Array(lib.boolexpr_Array_restrict(self._cdata, num, c_vars, c_consts))
def equiv(self, other):
"""Return True if two arrays are equivalent.
.. note:: While in practice this check can be quite fast,
SAT is an NP-complete problem, so some inputs will require
exponential runtime.
"""
return bool(lib.boolexpr_Array_equiv(self._cdata, other._cdata))
def zext(self, num):
"""Zero-extend this array by *num* bits."""
return Array(lib.boolexpr_Array_zext(self._cdata, num))
def sext(self, num):
"""Sign-extend this array by *num* bits."""
return Array(lib.boolexpr_Array_sext(self._cdata, num))
def nor_reduce(self):
"""Reduce items of the array using the NOR operator."""
return _bx(lib.boolexpr_Array_nor_reduce(self._cdata))
def or_reduce(self):
"""Reduce items of the array using the OR operator."""
return _bx(lib.boolexpr_Array_or_reduce(self._cdata))
def nand_reduce(self):
"""Reduce items of the array using the NAND operator."""
return _bx(lib.boolexpr_Array_nand_reduce(self._cdata))
def and_reduce(self):
"""Reduce items of the array using the AND operator."""
return _bx(lib.boolexpr_Array_and_reduce(self._cdata))
def xnor_reduce(self):
"""Reduce items of the array using the XNOR operator."""
return _bx(lib.boolexpr_Array_xnor_reduce(self._cdata))
def xor_reduce(self):
"""Reduce items of the array using the XOR operator."""
return _bx(lib.boolexpr_Array_xor_reduce(self._cdata))
def lsh(self, a):
"""Left shift operator"""
cdata = lib.boolexpr_Array_lsh(self._cdata, a._cdata)
return _ArrayPair(cdata).t
def rsh(self, a):
"""Right shift operator"""
cdata = lib.boolexpr_Array_rsh(self._cdata, a._cdata)
return _ArrayPair(cdata).t
def arsh(self, num):
"""Arithmetic right shift operator"""
cdata = lib.boolexpr_Array_arsh(self._cdata, num)
return _ArrayPair(cdata).t
[docs]def zeros(*dims):
"""Return a multi-dimensional array of zeros.
The variadic *dims* input is a sequence of dimension specs.
A dimension spec is a two-tuple: (start index, stop index).
If a dimension is given as a single ``int``,
it will be converted to ``(0, stop)``.
The dimension starts at index ``start``,
and increments by one up to, but not including, ``stop``.
This follows the Python slice convention.
For example, to create a 4x4 array of zeros::
>>> zeros(2, 4)
array([[0, 0, 0, 0],
[0, 0, 0, 0]])
"""
shape = _dims2shape(*dims)
objs = [ZERO for _ in range(_volume(shape))]
return array(objs, shape)
[docs]def ones(*dims):
"""Return a multi-dimensional array of ones.
The variadic *dims* input is a sequence of dimension specs.
A dimension spec is a two-tuple: (start index, stop index).
If a dimension is given as a single ``int``,
it will be converted to ``(0, stop)``.
The dimension starts at index ``start``,
and increments by one up to, but not including, ``stop``.
This follows the Python slice convention.
For example, to create a 4x4 array of ones::
>>> ones(2, 4)
array([[1, 1, 1, 1],
[1, 1, 1, 1]])
"""
shape = _dims2shape(*dims)
objs = [ONE for _ in range(_volume(shape))]
return array(objs, shape)
[docs]def logicals(*dims):
"""Return a multi-dimensional array of X's.
The variadic *dims* input is a sequence of dimension specs.
A dimension spec is a two-tuple: (start index, stop index).
If a dimension is given as a single ``int``,
it will be converted to ``(0, stop)``.
The dimension starts at index ``start``,
and increments by one up to, but not including, ``stop``.
This follows the Python slice convention.
For example, to create a 4x4 array of X's::
>>> logicals(2, 4)
array([[X, X, X, X],
[X, X, X, X]])
"""
shape = _dims2shape(*dims)
objs = [LOGICAL for _ in range(_volume(shape))]
return array(objs, shape)
def illogicals(*dims):
"""Return a multi-dimensional array of X's.
The variadic *dims* input is a sequence of dimension specs.
A dimension spec is a two-tuple: (start index, stop index).
If a dimension is given as a single ``int``,
it will be converted to ``(0, stop)``.
The dimension starts at index ``start``,
and increments by one up to, but not including, ``stop``.
This follows the Python slice convention.
For example, to create a 4x4 array of X's::
>>> illogicals(2, 4)
array([[?, ?, ?, ?],
[?, ?, ?, ?]])
"""
shape = _dims2shape(*dims)
objs = [ILLOGICAL for _ in range(_volume(shape))]
return array(objs, shape)
[docs]def uint2nda(num, length=None):
"""Convert unsigned *num* to an array of expressions.
The *num* argument is a non-negative integer.
If no *length* parameter is given,
the return value will have the minimal required length.
Otherwise, the return value will be zero-extended to match *length*.
For example, to convert the byte 42 (binary ``0b00101010``)::
>>> uint2nda(42, 8)
array([0, 1, 0, 1, 0, 1, 0, 0])
"""
if num < 0:
raise ValueError("expected num >= 0")
else:
objs = _uint2objs(num, length)
return array(objs)
[docs]def int2nda(num, length=None):
"""Convert *num* to an array of expressions.
The *num* argument is an ``int``.
Negative numbers will be converted using twos-complement notation.
If no *length* parameter is given,
the return value will have the minimal required length.
Otherwise, the return value will be sign-extended to match *length*.
For example, to convert the bytes 42 (binary ``0b00101010``),
and -42 (binary ``0b11010110``)::
>>> int2nda(42, 8)
array([0, 1, 0, 1, 0, 1, 0, 0])
>>> int2nda(-42, 8)
array([0, 1, 1, 0, 1, 0, 1, 1])
"""
if num < 0:
req_length = clog2(abs(num)) + 1
objs = _uint2objs(2**req_length + num)
else:
req_length = clog2(num + 1) + 1
objs = _uint2objs(num, req_length)
if length:
if length < req_length:
fstr = "overflow: num = {} requires length >= {}, got length = {}"
raise ValueError(fstr.format(num, req_length, length))
else:
sign = objs[-1]
objs += [sign] * (length - req_length)
return array(objs)
[docs]def array(objs, shape=None):
"""Return an N-dimensional array of Boolean expressions.
The *objs* argument is a nested sequence of homogeneous Boolean expressions.
That is, both [a, b, c, d] and [[a, b], [c, d]] are valid inputs.
The optional *shape* parameter is a tuple of dimension specs,
which are ``(int, int)`` tuples.
It must match the volume of *objs*.
The shape can always be automatically determined from *objs*,
but you can supply it to automatically reshape a flat input.
"""
items, autoshape = _itemize(objs)
if shape is None:
shape = autoshape
else:
_check_shape(shape)
if _volume(shape) != len(items):
raise ValueError("expected shape volume to match items")
num, c_bxs = _convert_args(items)
bxa = Array(lib.boolexpr_Array_new(num, c_bxs))
return ndarray(bxa, shape)
[docs]class ndarray: # pylint: disable=invalid-name
"""
N-dimensional array of Boolean expressions
"""
def __init__(self, bxa, shape=None):
self._bxa = bxa
if shape is None:
self._shape = ((0, len(bxa)), )
else:
self._shape = shape
self._ndim = len(self._shape)
self._nshape = tuple(stop - start for start, stop in self._shape)
self._strides = tuple(reduce(operator.mul, self._nshape[i+1:], 1)
for i in range(self._ndim))
@property
def bxa(self):
"""Return the Array object."""
return self._bxa
@property
def shape(self):
"""Return the Array shape."""
return self._shape
[docs] def reshape(self, *dims):
"""Return an equivalent array with a modified shape."""
shape = _dims2shape(*dims)
if _volume(shape) != self.size:
raise ValueError("expected shape with equal volume")
return self.__class__(self._bxa, shape)
def __str__(self):
pre, post = "array(", ")"
indent = " " * len(pre) + " "
return pre + self._str(indent) + post
def _str(self, indent=""):
"""Helper function for __str__"""
if self.ndim <= 1:
return "[" + ", ".join(str(x) for x in self) + "]"
elif self.ndim == 2:
sep = ",\n" + indent
# pylint: disable=protected-access
return "[" + sep.join(x._str(indent + " ") for x in self) + "]"
else:
sep = ",\n\n" + indent
# pylint: disable=protected-access
return "[" + sep.join(x._str(indent + " ") for x in self) + "]"
def __repr__(self):
return self.__str__()
def __iter__(self):
for i in range(self._shape[0][0], self._shape[0][1]):
yield self[i]
def __len__(self):
return self._shape[0][1] - self._shape[0][0]
def __getitem__(self, key):
parts = self._key2parts(key)
ranges, shape = self._walk_parts(parts)
items = list()
for coord in itertools.product(*ranges):
index = sum(self._strides[i] * coord[i] for i in range(self.ndim))
items.append(self._bxa[index])
if shape:
return array(items, tuple(shape))
else:
return items[0]
def __setitem__(self, key, val):
parts = self._key2parts(key)
ranges, _ = self._walk_parts(parts)
val = _expect_array(val)
coords = list(itertools.product(*ranges))
if val.size != len(coords):
fstr = "expected val.size = {}, got {}"
raise ValueError(fstr.format(len(coords), val.size))
for coord, _val in zip(coords, val.flat):
index = sum(self._strides[i] * coord[i] for i in range(self.ndim))
self._bxa[index] = _val
# Operators
def __invert__(self):
return self.__class__(~self._bxa, self._shape)
def __or__(self, other):
other = _expect_array(other)
shape = None
if self._shape == other.shape:
shape = self._shape
return self.__class__(self._bxa | other.bxa, shape)
def __and__(self, other):
other = _expect_array(other)
shape = None
if self._shape == other.shape:
shape = self._shape
return self.__class__(self._bxa & other.bxa, shape)
def __xor__(self, other):
other = _expect_array(other)
shape = None
if self._shape == other.shape:
shape = self._shape
return self.__class__(self._bxa ^ other.bxa, shape)
def __lshift__(self, num):
return self.lsh(zeros(num))[0]
def __rshift__(self, num):
return self.rsh(zeros(num))[1]
def __add__(self, other):
other = _expect_array(other)
bxa = self.bxa + other.bxa
shape = None
if (self.ndim == other.ndim > 1
and self._shape[1:] == other.shape[1:]
and self._shape[0][0] == other.shape[0][0] == 0):
shape0 = ((0, self._shape[0][1] + other.shape[0][1]), )
shape = shape0 + self._shape[1:]
return self.__class__(bxa, shape)
def __radd__(self, other):
return _expect_array(other) + self
def __mul__(self, num):
if num < 0:
raise ValueError("expected multiplier to be non-negative")
bxa = self.bxa * num
shape = None
if self.ndim > 1 and self._shape[0][0] == 0:
shape0 = ((0, self._shape[0][1] * num), )
shape = shape0 + self._shape[1:]
return self.__class__(bxa, shape)
def __rmul__(self, num):
return self.__mul__(num)
@property
def ndim(self):
"""Return the number of dimensions."""
return self._ndim
@property
def size(self):
"""Return the size of the array.
The *size* of a multi-dimensional array is the product of the sizes
of its dimensions.
"""
return len(self._bxa)
@property
def flat(self):
"""Return a 1D iterator over the array."""
return iter(self._bxa)
[docs] def simplify(self):
"""Apply the ``simplify`` method to all expressions.
Returns a new array.
"""
return self.__class__(self._bxa.simplify(), self._shape)
[docs] def compose(self, var2bx):
"""Apply the ``compose`` method to all expressions.
Returns a new array.
"""
var2bx = _vmap2map(var2bx)
return self.__class__(self._bxa.compose(var2bx), self._shape)
[docs] def restrict(self, point):
"""Apply the ``restrict`` method to all expressions.
Returns a new array.
"""
point = _vpnt2pnt(point)
return self.__class__(self._bxa.restrict(point), self._shape)
[docs] def equiv(self, other):
"""Return True if two arrays are equivalent.
.. note:: While in practice this check can be quite fast,
SAT is an NP-complete problem, so some inputs will require
exponential runtime.
"""
other = _expect_array(other)
return self._bxa.equiv(other.bxa)
[docs] def zext(self, num):
"""Zero-extend this array by *num* bits.
Returns a new array.
"""
return self.__class__(self._bxa.zext(num))
[docs] def sext(self, num):
"""Sign-extend this array by *num* bits.
Returns a new array.
"""
return self.__class__(self._bxa.sext(num))
# Reduction operators
[docs] def nor_reduce(self):
"""NOR reduction operator"""
return self._bxa.nor_reduce()
[docs] def or_reduce(self):
"""OR reduction operator"""
return self._bxa.or_reduce()
[docs] def nand_reduce(self):
"""NAND reduction operator"""
return self._bxa.nand_reduce()
[docs] def and_reduce(self):
"""AND reduction operator"""
return self._bxa.and_reduce()
[docs] def xnor_reduce(self):
"""XNOR reduction operator"""
return self._bxa.xnor_reduce()
[docs] def xor_reduce(self):
"""XOR reduction operator"""
return self._bxa.xor_reduce()
# Shift operators
[docs] def lsh(self, a):
"""Left shift the *a* array into this array.
Returns a two-tuple (array, array)
"""
a = _expect_array(a)
if not 0 <= a.size <= self.size:
fstr = "expected 0 <= a.size <= {}"
raise ValueError(fstr.format(self.size))
left, right = self._bxa.lsh(a.bxa)
return self.__class__(left), self.__class__(right)
[docs] def rsh(self, a):
"""Right shift the *a* array into this array.
Returns a two-tuple (array, array)
"""
a = _expect_array(a)
if not 0 <= a.size <= self.size:
fstr = "expected 0 <= a.size <= {}"
raise ValueError(fstr.format(self.size))
left, right = self._bxa.rsh(a.bxa)
return self.__class__(left), self.__class__(right)
[docs] def arsh(self, num):
"""Arithmetically right shift the array by *num* places.
The *num* argument must be a non-negative ``int``.
The shift-in will be the value of the most significant bit.
Returns a two-tuple (array, array)
"""
if not 0 <= num <= self.size:
raise ValueError("expected 0 <= num <= {}".format(self.size))
left, right = self._bxa.arsh(num)
return self.__class__(left), self.__class__(right)
# Subroutines of __getitem__, __setitem__
@staticmethod
def _part(item):
if item is Ellipsis:
return item
elif isinstance(item, slice):
return item
else:
return operator.index(item)
def _key2parts(self, key):
"""Convert input key to a list of index parts."""
if isinstance(key, tuple):
parts = [self._part(item) for item in key]
else:
parts = [self._part(key)]
if len(parts) > self.ndim:
fstr = "expected <= {0.ndim} slice dimensions, got {1}"
raise ValueError(fstr.format(self, len(parts)))
parts = self._fill_parts(parts)
parts = self._norm_parts(parts)
return parts
def _fill_parts(self, parts):
"""Fill all '...' and ':' slice entries."""
# Fill '...' entries with ':'
nfill = self.ndim - len(parts)
fparts = list()
for part in parts:
if part is Ellipsis:
while nfill:
fparts.append(slice(None, None))
nfill -= 1
fparts.append(slice(None, None))
else:
fparts.append(part)
# Append ':' to the end
for _ in range(self.ndim - len(fparts)):
fparts.append(slice(None, None))
return fparts
def _norm_parts(self, parts):
"""Normalize all key parts to array shape."""
nparts = list()
for i, part in enumerate(parts):
if isinstance(part, int):
nparts.append(self._norm_int(i, part))
elif isinstance(part, slice):
nparts.append(self._norm_slice(i, part))
else:
assert False # pragma: no cover
return nparts
def _norm_int(self, i, part):
if part < 0:
npart = part + self._nshape[i]
else:
npart = part - self._shape[i][0]
if not 0 <= npart < self._nshape[i]:
raise IndexError("ndarray index out of range")
return npart
def _norm_slice(self, i, part):
if part.start is not None and part.start >= 0:
start = part.start - self._shape[i][0]
if start < 0:
raise IndexError("ndarray slice out of range")
else:
start = part.start
if part.stop is not None and part.stop >= 0:
stop = part.stop - self._shape[i][0]
if stop < 0:
raise IndexError("ndarray slice out of range")
else:
stop = part.stop
if part.step is not None:
raise ValueError("slice step is not supported")
return slice(start, stop)
def _walk_parts(self, parts):
"""Walk through slice parts, and get characteristic info."""
ranges = list()
shape = list()
for i, part in enumerate(parts):
if isinstance(part, int):
ranges.append(range(part, part+1))
elif isinstance(part, slice):
start, stop, _ = part.indices(self._nshape[i])
ranges.append(range(start, stop))
shape.append((self._shape[i][0] + start,
self._shape[i][0] + stop))
else:
assert False # pragma: no cover
return ranges, shape
def _check_dim(dim):
"""Verify that a dimension has the right format."""
if not 0 <= dim[0] <= dim[1]:
raise ValueError("expected 0 <= low <= high dimensions")
def _check_shape(shape):
"""Verify that a shape has the right format."""
if isinstance(shape, tuple):
for dim in shape:
if _is_dim(dim):
_check_dim(dim)
else:
raise TypeError("expected shape dimension to be (int, int)")
else:
raise TypeError("expected shape to be tuple of (int, int)")
def _dims2shape(*dims):
"""Convert input dimensions to a shape."""
if not dims:
raise ValueError("expected at least one dimension spec")
shape = list()
for dim in dims:
if isinstance(dim, int):
start, stop = (0, dim)
elif _is_dim(dim):
_check_dim(dim)
start, stop = dim
else:
raise TypeError("expected dimension to be int or (int, int)")
shape.append((start, stop))
return tuple(shape)
def _flatten(var_or_seq, val, f):
"""Recursively flatten vectorized var => bx mappings."""
if isinstance(var_or_seq, Variable):
yield var_or_seq, f(val)
else:
if len(var_or_seq) != len(val):
raise ValueError("expected 1:1 mapping from Variable => {0, 1}")
for _var_or_seq, _val in zip(var_or_seq, val):
yield from _flatten(_var_or_seq, _val, f)
def _is_dim(obj):
"""Return True if the object is a shape dimension."""
return (isinstance(obj, tuple) and len(obj) == 2
and isinstance(obj[0], int) and isinstance(obj[1], int))
def _itemize(objs):
"""Recursive helper function for array."""
isseq = [isinstance(obj, collections.Sequence) for obj in objs]
if not any(isseq):
return list(objs), ((0, len(objs)), )
elif all(isseq):
items = list()
shape = None
for obj in objs:
_items, _shape = _itemize(obj)
if shape is None:
shape = _shape
elif shape != _shape:
raise ValueError("expected uniform array dimensions")
items += _items
shape = ((0, len(objs)), ) + shape
return items, shape
else:
raise ValueError("expected uniform array dimensions")
def _uint2objs(num, length=None):
"""Convert an unsigned integer to a list of constant expressions."""
if num == 0:
objs = [ZERO]
else:
_num = num
objs = list()
while _num != 0:
objs.append(ONE if _num & 1 else ZERO)
_num >>= 1
if length:
if length < len(objs):
fstr = "overflow: num = {} requires length >= {}, got length = {}"
raise ValueError(fstr.format(num, len(objs), length))
else:
while len(objs) < length:
objs.append(ZERO)
return objs
def _volume(shape):
"""Return the volume of a shape."""
prod = 1
for start, stop in shape:
prod *= stop - start
return prod
def _vmap2map(vmap):
"""Convert *vmap* into a var2bx in an N-dimensional Boolean space."""
var2bx = dict()
for var_or_seq, val in vmap.items():
var2bx.update(_flatten(var_or_seq, val, _expect_bx))
return var2bx
def _vpnt2pnt(vpnt):
"""Convert *vpnt* into a point in an N-dimensional Boolean space.
The *vpnt* argument is a mapping from multi-dimensional arrays of
variables to matching arrays of :math:`{0, 1}`.
Elements from the values array will be converted to :math:`{0, 1}` using
the `int` builtin function.
"""
point = dict()
for var_or_seq, val in vpnt.items():
point.update(_flatten(var_or_seq, val, _expect_const))
return point