Quick Start¶
This chapter will get you up and running with BoolExpr. We will discuss how to create Boolean expressions, and some of their most essential properties.
See the Installation chapter for how to install BoolExpr. We will assume you have installed the library, and have executed the following in your Python REPL of choice (recommend using IPython).
>>> import boolexpr as bx
Creating Variables¶
In order to construct Boolean functions, we must first create some symbolic variables.
Let’s create four symbols: \(w, x, y, z\).
>>> ctx = bx.Context()
>>> w = ctx.get_var("w")
>>> type(w)
boolexpr.Variable
>>> x, y, z = map(ctx.get_var, "xyz")
Variable Context¶
If you are familiar with other symbolic algebra systems,
the first statement ctx = bx.Context()
will be confusing.
A variable “context” is a kind of namespace container for Boolean variables.
It manages the creation and storage of variables.
No two variables within the same context can have the same name.
Therefore, asking a Context
object for the same
name will always return the same object.
>>> ctx.get_var("foo") is ctx.get_var("bar")
False
>>> ctx.get_var("foo") is ctx.get_var("foo")
True
Variable Naming¶
You can name variables any string that has an ASCII encoding.
(since the C++ layer uses std::string
for the variable name type).
The following are all valid variable names:
>>> x = ctx.get_var("x")
>>> x_0 = ctx.get_var("x_0")
>>> z = ctx.get_var("Zebra")
>>> rbbb = ctx.get_var("Rubber Baby Buggy Bumpers")
>>> hrm = ctx.get_var("This !@#$ is %^&* legal -+=| too")
But using Unicode will throw an exception.
>>> nope = ctx.get_var("\u0394")
UnicodeEncodeError: 'ascii' codec can't encode character '\u0394'
in position 0: ordinal not in range(128)
Tips and Tricks¶
It’s a common operation to create more than one variable at a time.
For this you can use the get_vars
method.
For example, to create a list of eight \(x\) variables with an index:
>>> xs = ctx.get_vars('x', 8)
>>> xs
array([x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]])
>>> xs[3]
x[3]
Provide more than one argument to create a multi-dimensional array. For example, to create a 4x4 list of \(x\) variables:
>>> xs = ctx.get_vars('x', 4, 4)
>>> xs
array([[x[0,0], x[0,1], x[0,2], x[0,3]],
[x[1,0], x[1,1], x[1,2], x[1,3]],
[x[2,0], x[2,1], x[2,2], x[2,3]],
[x[3,0], x[3,1], x[3,2], x[3,3]]])
>>> xs[2,3]
x[2,3]
>>> xs[2][3]
x[2,3]
Creating Expressions¶
This section covers the various ways to construct expression trees.
Overloaded Python Operators¶
BoolExpr overloads the Python “bitwise” operators,
~ | & ^
to mean NOT, OR, AND, and XOR, respectively.
This allows you to construct expressions with the most common logical operators
in a domain specific language.
For example, the following code will create an expression, \(f\):
>>> f = ~w | x & ~y ^ z
>>> f
Or(~w, Xor(And(x, ~y), z))
The name f
is a Python handle.
The expression object itself is just a pointer,
and has no intrinsic name.
In graphical form, the function \(f\) looks like this:
Constant Inputs¶
To use constant zero and one atoms,
use either False/True
, or 0/1
in the expression.
>>> w | False
Or(w, 0)
>>> True & x
And(1, x)
>>> 0 ^ y ^ 1
Xor(Xor(0, y), 1)
Zero and one are singletons within the boolexpr
module.
If you really need access to them for some reason,
use the ZERO
and ONE
handles.
>>> bx.ZERO | bx.ONE
Or(0, 1)
There is also a constant called “logical”.
It represents a constant value of either zero or one.
Since there is no handy Python analog to this value,
you can use either 'x'
, 'X'
, or LOGICAL
as a fill-in.
The notation X
comes from Verilog four-state logic.
>>> w | 'X'
Or(w, X)
>>> bx.LOGICAL & w
And(X, w)
Equal, Implies, and If-Then-Else¶
BoolExpr supports the “unequal, “equal”, “implies” and “if-then-else”
symbolic operators.
Python does not have good symbols to use for these,
so you must use the neq
,
eq
, impl
,
and ite
functions.
>>> bx.neq(~y, z)
Unequal(~y, z)
>>> bx.eq(y, ~z)
Equal(y, ~z)
>>> p, q = map(ctx.get_var, "pq")
>>> bx.impl(p, q)
Implies(p, q)
>>> s, d1, d0 = map(ctx.get_var, "s d1 d0".split())
>>> bx.ite(s, d1, d0)
IfThenElse(s, d1, d0)
The rules for constants are the same as in the previous section.
>>> bx.impl(p, False)
Implies(p, 0)
>>> bx.ite(True, d1, 'X')
IfThenElse(1, d1, X)
Nary Operator Functions¶
One disadvantage of using Python’s builtin operators is that they only allow you to create binary trees. But the OR, AND, and XOR operators are N-ary operators, which means they take an arbitrary number of arguments, \(N\).
To construct expressions with flat, N-ary operators,
use the or_
,
and_
,
and xor
functions.
>>> bx.or_(w, x, y, z)
Or(w, x, y, z)
>>> bx.and_(w, False, y, True)
And(w, 0, y, 1)
>>> bx.xor(w|x, y&z, bx.impl(p,q))
Xor(Or(w, x), And(y, z), Implies(p, q))
In addition,
the nor
,
nand
,
and xnor
functions provide the “negative” form of
these N-ary operators.
Simplification¶
In the previous sections, you may have noticed places where we created Boolean expressions with obvious simplifications. For example, we know that \(x \cdot 0 \iff 0\), but writing out that equation will produce the following:
>>> x & False
And(x, 0)
BoolExpr purposefully does not automatically simplify these expressions by
default,
but you can use the simplify
method to
get the more obvious output.
>>> f = x & False
>>> f.simplify()
0
The simplify
method attempts to perform
all sorts of transformations
with the goal of getting rid of constants,
and sub-expressions that can easily be proven equivalent to constants.
Sometimes, you might wish for the default behavior to automatically simplify. For this, every operator function has a corresponding auto-simplify form:
Basic Op | Simplifying Op |
---|---|
nor |
nor_s |
or_ |
or_s |
nand |
nand_s |
and_ |
and_s |
xnor |
xnor_s |
xor |
xor_s |
neq |
neq_s |
eq |
eq_s |
impl |
impl_s |
ite |
ite_s |
Function Domain and Range¶
The most basic way to understand a Boolean function is to examine its “truth table”, a list of how all possible input assignments map to output assignments.
First,
given some arbitrary expression f
,
what variables does it depend on?
This set is often called the support set of the function.
To get it, use the support
method:
>>> f = ~w | x & ~y ^ z
>>> f.support()
{y, w, z, x}
A Boolean function is a rule that maps points in an \(N\)-dimensional Boolean space to an element in \(\{0, 1\}\). In formal mathematical lingo, \(f: B^N \Rightarrow B\), where \(B^N\) means the Cartesian product of \(N\) sets of type \(\{0, 1\}\). For example, if you have three input variables, \(a, b, c\), each defined on \(\{0, 1\}\), then \(B^3 = \{0, 1\}^3 = \{(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)\}\). \(B^3\) is the domain of the function (the input part), and \(B = \{0, 1\}\) is the range of the function (the output part).
Use the iter_domain
generator method
to iterate through all points in the domain,
The restrict
method evaluates the output
value of a function at one particular input point.
The combination of these two methods produces a truth table:
>>> for point in f.iter_domain():
print(point, f.restrict(point))
{y: 0, w: 0, z: 0, x: 0} 1
{y: 0, w: 0, z: 1, x: 0} 1
{y: 1, w: 0, z: 0, x: 0} 1
{y: 1, w: 0, z: 1, x: 0} 1
{y: 0, w: 0, z: 0, x: 1} 1
{y: 0, w: 0, z: 1, x: 1} 1
{y: 1, w: 0, z: 0, x: 1} 1
{y: 1, w: 0, z: 1, x: 1} 1
{y: 0, w: 1, z: 0, x: 0} 0
{y: 0, w: 1, z: 1, x: 0} 1
{y: 1, w: 1, z: 0, x: 0} 0
{y: 1, w: 1, z: 1, x: 0} 1
{y: 0, w: 1, z: 0, x: 1} 1
{y: 0, w: 1, z: 1, x: 1} 0
{y: 1, w: 1, z: 0, x: 1} 0
{y: 1, w: 1, z: 1, x: 1} 1