Expand description
Advanced BDD operations
So far, we have seen how to perform standard logical operations on BDDs, create formulas from CNF/DNF representations and inspect individual valuations/clauses of a BDD. However, sometimes (especially when a BDD is used to represent a relation), we also need other, more specialised operations.
Selection
Selection is the simplest of operations discussed here. Essentially, selection restricts
a Bdd
to valuations which have values of specific variables fixed to specific constants:
use biodivine_lib_bdd::BddVariableSet;
let vars = BddVariableSet::new_anonymous(5);
let variables = vars.variables();
let bdd = vars.eval_expression_string("(x_0 & !x_1) | (!x_0 & x_3)");
let select_x0_true = bdd.var_select(variables[0], true);
let select_x0_false = bdd.var_select(variables[0], false);
assert_eq!(vars.eval_expression_string("x_0 & !x_1"), select_x0_true);
assert_eq!(vars.eval_expression_string("!x_0 & x_3"), select_x0_false);
This is essentially the same as executing Bdd.and
with an appropriate
BddVariableSet.mk_literal
as argument. However, aside from Bdd.var_select
,
there is also Bdd.select
which accepts multiple variables and executes as a single
symbolic operation. Hence it is more efficient than calling Bdd.and
repeatedly
for multiple variables.
Note that (contrary to how selection usually works in relational algebras), the selected
variables remain in the Bdd
. In a relational algebra, a selection on x
typically results
in a set of vectors that do not contain x
any more (since the value is fixed). Here, the
resulting Bdd
still conditions on variable x
, but it always requires x
to have
the value specified during selection.
Projection
Projection is one of the most fundamental BDD operations. There are two “ways” of explaining projection – depending on your background, one may seem more intuitive than the other.
For simplicity, we are going to project using only a single variable (
Bdd.var_project
), but as in the case of selection, there is also a multi-variable projection operation available (Bdd.project
).
First, a “logical” approach says that a projection of a BDD through variable x
is equivalent
to existential quantification in first order logic. So, if B
is a BDD, and $\varphi$ is
a formula that is represented by B
, then B' = B.var_project(x)
represents a formula
$\varphi’ = \exists x. \varphi$. Consequently B'
does not depend on variable x
in any
way. Which leads us to the second explanation.
A “relational” approach says that projection is elimination. If we see the BDD B
as
a collection of valuations that satisfy $\varphi$, then projection eliminates x
from
each valuation. For example, if B
is satisfied for (x=true, y=true, z=false)
, then
B.var_project(x)
is satisfied for (y=true, z=false)
, regardless of x
. However, since
the variable is not removed from the overall BddVariableSet
, the satisfying valuations
will still contain x
, the satisfiability of the Bdd
will simply never depend on it.
That is, if (x=true, y=true, z=false)
is a satisfying valuation, then
(x=false, y=true, z=false)
must be as well.
use biodivine_lib_bdd::BddVariableSet;
let vars = BddVariableSet::new_anonymous(5);
let variables = vars.variables();
let bdd = vars.eval_expression_string("(x_0 & !x_1) | (!x_0 & x_3)");
let projected = bdd.var_project(variables[0]);
assert_eq!(vars.eval_expression_string("!x_1 | x_3"), projected);
let projected = bdd.project(&[variables[0], variables[1]]);
assert_eq!(vars.mk_true(), projected);
Pick
Finally, the most “specialised” operation is Bdd.pick
(or, again Bdd.var_pick
when
only one variable is involved). Picking is similar to selection in that it fixes the
value of a variable. However, the outcome is slightly different:
Say that V
and V'
are both satisfying valuations of a BDD B
, and they are equivalent
except that V(x) = true
and V(x) = false
. Then, if B' = B.var_pick(x)
, V'
will be
a satisfying valuation of B'
, but not V
. That is, B'
just picked one of the two
valuations. If only one of V
and V'
satisfies B
, the operation will retain the one
satisfying valuation (i.e. again, picks the appropriate satisfying valuation out of the two).
However, for pick operation, there is an important distinction. Whereas
B.var_select(x, true).var_select(y, false) = B.select(&[(x, true), (y, false)])
and
B.var_project(x).var_project(y) = B.project(&[x,y])
, this does not hold for picking.
If we call B.var_pick(x).var_pick(y)
, we are saying that we pick one value of x
for each
valuation of remaining variables, and then similarly pick one value of y
, meaning that picking
a value of x
is independent of y
and vice versa. Meanwhile, B.pick(&[x,y])
denotes that
one valuation of both x
and y
should be picked for each valuation of the remaining variables.
That is, x
and y
are tied together in the pick operation.
Using pick
, we can implement various interesting relational operations. For example, say that
a BDD represents a relation where i_1
and i_2
are some “inputs” of the relation and o_1, o_2
are some “outputs”. Then we can use pick(i_1, i_2)
to select one unique input for each output
value in the relation. This effectively picks an injective relation which is a subset of the original
relation. Similarly, using pick(o_1, o_2)
, we can get a sub-relation which is a valid
partial function (each input valuation has at most one output).
use biodivine_lib_bdd::BddVariableSet;
let vars = BddVariableSet::new_anonymous(5);
let variables = vars.variables();
let bdd = vars.eval_expression_string("(x_0 => (x_1 & x_2)) & (!x_0 => x_3)");
// In the first branch, x_0 = true, x_1 is fixed to true, so there is already at most one
// valuation. In the second branch (x_0 = false), the operation will fix x_1 to false in order
// to pick one of the two available values.
let picked = bdd.var_pick(variables[1]);
let expected = vars.eval_expression_string("(x_0 => (x_1 & x_2)) & (!x_0 => (!x_1 & x_3))");
assert_eq!(expected, picked);
Finally, note that pick
and var_pick
prioritise picking valuations where the variables are
fixed to false
. If you want to pick randomly, you can use pick_random
and var_pick_random
.
Just keep in mind that this operation will not pick a new random value for each possible valuation
of the remaining variables, it will just pick a random “basal value” a variable will prefer,
instead of defaulting to false
.