3

Say I have the following Boolean functions:

or(x, y) := x || y
and(x, y) := x && y
not(x) := !x
foo(x, y, z) := and(x, or(y, z))
bar(x, y, z, a) := or(foo(x, y, z), not(a))
baz(x, y) := and(x, not(y))

Now I would like to construct a Binary Decision Diagram from them. I have looked through several papers but haven't been able to find how to construct them from nested logic formulas like these.

It is said that a Boolean function is a rooted, directed, acyclic graph. It has several nonterminal and terminal nodes. Then it says that each nonterminal node is labeled by a Boolean variable (not a function), which has two child nodes. I don't know what a Boolean variable is from my function definitions above. An edge from the node to child a or b represents assigment of the node to 0 or 1 respectively. It is called reduced if isomorphic subgraphs have been merged, and nodes whose two children are isomorphic are removed. This is a Reduced Ordered Binary Decision Diagram (ROBDD).

From that, and from all of the resources I've encountered, I haven't been able to figure out how to convert this these functions into BDDs/ROBDDs:

foo(1, 0, 1)
bar(1, 0, 1, 0)
baz(1, 0)

Or perhaps it's these that need to be converted:

foo(x, y, z)
bar(x, y, z, a)
baz(x, y)

Looking for help on an explanation of what I need to do in order to make this into the rooted, directed, acyclic graph. Knowing what the data structure looks like would also be helpful. It seems that it is just this:

var nonterminal = {
  a: child,
  b: child,
  v: some variable, not sure what
}

But then the question is how to go about constructing the graph from these functions foo, bar, and baz.

Lance
  • 75,200
  • 93
  • 289
  • 503

3 Answers3

2

The basic logic operations AND, OR, XOR etc can all be computed between functions that are in BDD representation to yield a new function in BDD representation. The algorithms for these are all similar apart from how they handle terminals, and roughly goes like this:

  • if the result is a terminal, return that terminal.
  • if (op, A, B) is cached, return the cached result.
  • distinguish 3 cases (actually you can generalize this..)

    1. A.var == B.var, create a node (A.var, OP(A.lo, B.lo), OP(A.hi, B.hi)) where OP represents recursively invoking this procedure.
    2. A.var < B.var, create a node (A.var, OP(A.lo, B), OP(A.hi, B))
    3. A.var > B.var, create a node (B.var, OP(A, B.lo), OP(A, B.hi))
  • cache the result

"Create a node" should of course deduplicate itself, to fulfill the "reduced" requirement. The split in 3 cases takes care of the ordering requirement.

Complex functions that are a tree of simple operations can be turned in a BDD by applying this bottom up, at every turn only doing a simple combination of BDDs. This of course does tend generate nodes that are not part of the final result. Variables and constants have trivial BDDs.

For example, and(x, or(y, z)) is created by going depth-first into that tree, creating a BDD for the variable x (a trivial node, (x, 0, 1)), then for y and z, performing OR (an instance of the algorithm described above, where only the first step really cares that the operation is OR) on the BDDs that represent y and z, and then performing AND on the result and the BDD the represents the variable x. The exact result depends on your choice of variable ordering.

Functions that evaluate other functions inside of themselves require either function composition (if representing the called function by a BDD already) which is possible with BDDs but has some bad worst cases, or just inline the definition of the called function.

harold
  • 61,398
  • 6
  • 86
  • 164
  • Not sure what `OP` means in `(A.var, OP(A.lo, B.lo), OP(A.hi, B.hi))`. Not quite sure what you mean by "Complex functions that are a tree of simple operations can be turned in a BDD by applying this bottom up, at every turn only doing a simple combination of BDDs." If you could provide an example that would be helpful. – Lance May 29 '18 at 23:49
  • @LancePollard ok I added an example. Btw you might also like http://haroldbot.nl/how.html – harold May 29 '18 at 23:54
  • Thank you for your help. – Lance May 30 '18 at 03:55
  • Is it true, that for full 32-bit adder this procedure will produce BDD that will be 'exponentially' big? And moreover, is it ever possible to get BDD for a 32-bit full adder that has a small size (i.e. < 1000 nodes)? – komorra Feb 03 '20 at 19:56
  • @komorra that depends on the variable order. If you put the higher order bits near the root of the graph, then an adder only has a linear number of nodes, only 3 per bit actually (minus a couple near the start). – harold Feb 04 '20 at 03:52
1

You can do it by evaluation of all variable assignments, e.g. in case of

foo(0,0,0) = 0
foo(0,0,1) = 0
foo(0,1,0) = 0
...

Then create the graph, start from the root. Each function argument gets an edge labeled with its assignment, the leaf node gets labeled with the result value:

x0 -0-> y0 -0-> z0 -0-> 0
x0 -0-> y1 -0-> z1 -1-> 0
x0 -0-> y2 -1-> z2 -0-> 0
...

merge the nodes (y0 = y1 = y2, z0 = z1):

x0 -0-> y0 -0-> z0 -0-> 0
x0 -0-> y0 -0-> z0 -1-> 0
x0 -0-> y0 -1-> z1 -0-> 0
...

reduce the nodes (There are some rules that allow to join nodes or to skip nodes). E.g. since a 0 from the root always leads to the leaf 0 you can skip the later decisions:

x0 -0-> 0
...

Note that the nodes have to be labeled with the variable names to be assigned on the following graph edges. The algorithm is not really sophisticated (certainly there exists a more efficient one), but I hope it visualizes the strategy.

CoronA
  • 7,717
  • 2
  • 26
  • 53
  • Wondering if you are saying you _must_ precompute the values of all functions. – Lance May 29 '18 at 23:45
  • It would be helpful to see what a tree looked like, maybe with notation like `x1(x2(...))`, or `var top = { data: ..., child1: ..., child2: ... }` not quite sure what the structure is you're describing. – Lance May 29 '18 at 23:47
0

A data structure for storing BDDs can be based on triplets of the form (level, u, v) where u the node for what the Boolean function is when the variable at level is FALSE, and v the node for what the Boolean function is when the variable at level is TRUE.

The example described can be programmed using the Python package dd (pip install dd to install the pure-Python implementation). The code would be

from dd import autoref as _bdd

bdd = _bdd.BDD()
bdd.declare('x', 'y', 'z', 'a')
x_or_y = bdd.add_expr(r'x \/ y')
x_and_y = bdd.add_expr(r'x /\ y')
not_x = bdd.add_expr('~ x')

# variable renaming: x to y, y to z
let = dict(x='y', y='z')
y_or_z = bdd.let(let, x_or_y)

# using the method `BDD.apply`
foo = bdd.apply('and', bdd.var('x'), y_or_z)

# using a formula
foo_ = bdd.add_expr(r'x /\ (y \/ z)')
assert foo == foo_

# using the string representation of BDD node references
foo_ = bdd.add_expr(rf'x /\ {y_or_z}')
assert foo == foo_

bar = bdd.apply('or', foo, ~ bdd.var('a'))
bar_ = bdd.add_expr(rf'{foo} \/ ~ a')

assert bar == bar_

let = dict(y= ~ bdd.var('y'))
baz = bdd.let(let, x_and_y)
baz_ = bdd.add_expr(r'x /\ ~ y')
assert baz == baz_
# plotting of the diagram using GraphViz
bdd.dump('foo.png', [foo])

This example includes both direct application of operators between BDDs (using the method BDD.apply or the parsing of Boolean formulas that calls these operators) and function composition for functions represented as BDDs (using the method BDD.let for renaming variables and for substituting a BDD for a variable, and the syntax f'{u}' for the string representation of a reference to a BDD node).

The result of plotting the Boolean function foo is shown below (produced by the bdd.dump statement in the above code).

The result of plotting foo

0 _
  • 10,524
  • 11
  • 77
  • 109