Since we now require Python 3.6, we can use f-strings instead of format() to make the code more readable. * doc/org/tut01.org, doc/org/tut02.org, doc/org/tut03.org, doc/org/tut21.org, doc/org/tut24.org, doc/org/tut90.org, python/spot/__init__.py, python/spot/jupyter.py, tests/python/acc.py, tests/python/acc_cond.ipynb, tests/python/complement_semidet.py, tests/python/decompose.ipynb, tests/python/formulas.ipynb, tests/python/highlighting.ipynb, tests/python/ipnbdoctest.py, tests/python/ltlf.py, tests/python/parity.ipynb, tests/python/product.ipynb, tests/python/relabel.py, tests/python/satmin.ipynb, tests/python/stutter-inv.ipynb, tests/python/twagraph-internals.ipynb, tests/python/zlktree.ipynb: Use f-strings.
452 lines
15 KiB
Org Mode
452 lines
15 KiB
Org Mode
# -*- coding: utf-8 -*-
|
|
#+TITLE: The bdd_dict interface (advanced topic)
|
|
#+DESCRIPTION: Description of the bdd_dict interface.
|
|
#+INCLUDE: setup.org
|
|
#+HTML_LINK_UP: tut.html
|
|
#+PROPERTY: header-args:C+++ :results verbatim :exports both
|
|
#+PROPERTY: header-args:python :results output :exports both
|
|
|
|
Spot uses BDD for multiple purposes.
|
|
|
|
The most common one is for labeling edges of automata: each edge
|
|
stores a BDD representing its guard (i.e., a Boolean function over
|
|
atomic propositions). Note that the automaton is still represented as
|
|
a graph (with a [[https://spot.lrde.epita.fr/ipynb/twagraph-internals.html][vector of states and a vector of edges]]) and the BDD is
|
|
only used for the guard. This differs from symbolic representations
|
|
where the entire transition structure is represented as one large BDD.
|
|
|
|
There are other algorithms where BDDs are used from different tasks.
|
|
For instance, our simulation-based reduction function computes a
|
|
*signature* of each state as a BDD that is essentially the disjunction
|
|
of all outgoing edges, represented by their guard, their acceptance
|
|
sets, and their destination *classes*. Also, the translation of LTL
|
|
formulas to transition-based generalized Büchi automata is using an
|
|
intermediate representation of states that is similar to the
|
|
aforementioned signatures, excepts that classes are replaced by
|
|
subformulas.
|
|
|
|
From the point of view of the BDD library, BDDs are just DAGs with
|
|
nodes labeled by BDD variables (numbered from 0). From the point of
|
|
view of Spot's algorithm, these BDD variables have a meaning. For
|
|
instance if we want to synchronize two automata that have guards over
|
|
the atomic propositions $a$ and $b$, we need to make sure that both
|
|
automata agree on the BDD variables used to represent $a$ and $b$.
|
|
|
|
* The purpose of =bdd_dict=
|
|
|
|
The =spot::bdd_dict= object is in charge of allocating BDD variables,
|
|
and ensuring that multiple users reuse the same variables for similar
|
|
purpose. When a =twa_graph= automaton is constructed, it takes a
|
|
=bdd_dict= as argument. Every time an atomic proposition is
|
|
registered through the =twa::register_ap()= method, the =bdd_dict=
|
|
is queried.
|
|
|
|
As an example, the following two automata share their =bdd_dict=, and
|
|
although they do not declare their atomic propositions in the same
|
|
order, they get compatible variable numbers.
|
|
|
|
#+BEGIN_SRC C++
|
|
#include <spot/twa/twagraph.hh>
|
|
|
|
int main()
|
|
{
|
|
spot::bdd_dict_ptr dict = spot::make_bdd_dict();
|
|
spot::twa_graph_ptr aut1 = spot::make_twa_graph(dict);
|
|
int ap1a = aut1->register_ap("a");
|
|
int ap1b = aut1->register_ap("b");
|
|
std::cout << "aut1: a=" << ap1a << " b=" << ap1b << '\n';
|
|
|
|
spot::twa_graph_ptr aut2 = spot::make_twa_graph(dict);
|
|
int ap2c = aut2->register_ap("c");
|
|
int ap2b = aut2->register_ap("b");
|
|
int ap2a = aut2->register_ap("a");
|
|
std::cout << "aut2: a=" << ap2a << " b=" << ap2b << " c=" << ap2c << '\n';
|
|
}
|
|
#+END_SRC
|
|
|
|
#+RESULTS:
|
|
: aut1: a=0 b=1
|
|
: aut2: a=0 b=1 b=2
|
|
|
|
Contrast the above result with the following example, where the two
|
|
automata use different =bdd_dict=:
|
|
|
|
|
|
#+BEGIN_SRC C++
|
|
#include <spot/twa/twagraph.hh>
|
|
|
|
int main()
|
|
{
|
|
spot::bdd_dict_ptr dict1 = spot::make_bdd_dict();
|
|
spot::twa_graph_ptr aut1 = spot::make_twa_graph(dict1);
|
|
int ap1a = aut1->register_ap("a");
|
|
int ap1b = aut1->register_ap("b");
|
|
std::cout << "aut1: a=" << ap1a << " b=" << ap1b << '\n';
|
|
|
|
spot::bdd_dict_ptr dict2 = spot::make_bdd_dict();
|
|
spot::twa_graph_ptr aut2 = spot::make_twa_graph(dict2);
|
|
int ap2c = aut2->register_ap("c");
|
|
int ap2b = aut2->register_ap("b");
|
|
int ap2a = aut2->register_ap("a");
|
|
std::cout << "aut2: a=" << ap2a << " b=" << ap2b << " c=" << ap2c << '\n';
|
|
}
|
|
#+END_SRC
|
|
|
|
#+RESULTS:
|
|
: aut1: a=0 b=1
|
|
: aut2: a=2 b=1 c=0
|
|
|
|
For this reason, operations like ~spot::product(aut1, aut2)~ will
|
|
require that ~aut1->get_dict() == aut2->get_dict()~.
|
|
|
|
|
|
In Python, many functions that would take an explicit =bdd_dict= in C++ will
|
|
default to some global =bdd_dict= instead. So we can do:
|
|
|
|
#+BEGIN_SRC python
|
|
import spot
|
|
aut1 = spot.make_twa_graph()
|
|
ap1a = aut1.register_ap("a")
|
|
ap1b = aut1.register_ap("b")
|
|
print(f"aut1: a={ap1a} b={ap1b}")
|
|
aut2 = spot.make_twa_graph()
|
|
ap2c = aut2.register_ap("c")
|
|
ap2b = aut2.register_ap("b")
|
|
ap2a = aut2.register_ap("a")
|
|
print(f"aut1: a={ap2a} b={ap2b} c={ap2c}")
|
|
#+END_SRC
|
|
|
|
#+RESULTS:
|
|
: aut1: a=0 b=1
|
|
: aut1: a=0 b=1 c=2
|
|
|
|
In that case we did not mention any =bdd_dict=, but there is one that
|
|
is implicitly used in both cases. Similarly, when we call
|
|
=spot.translate()= the same global =bdd_dict= is used by default.
|
|
|
|
What really confuses people, is that the association between an atomic
|
|
proposition (=a=, =b=, ...) and a BDD variable (=0=, =1=, ...) will
|
|
only be held by the =bdd_dict= for the lifetime of the objects (here the
|
|
automata) that registered this association to the =bdd_dict=.
|
|
|
|
Here is a new C++ example where we use the =bdd_dict::dump()= method
|
|
to display the contents of the =bdd_dict= (this method is only meant
|
|
for debugging, please do not rely on its output).
|
|
|
|
#+BEGIN_SRC C++
|
|
#include <spot/twa/twagraph.hh>
|
|
|
|
int main()
|
|
{
|
|
spot::bdd_dict_ptr dict = spot::make_bdd_dict();
|
|
|
|
spot::twa_graph_ptr aut1 = spot::make_twa_graph(dict);
|
|
int ap1a = aut1->register_ap("a");
|
|
int ap1b = aut1->register_ap("b");
|
|
std::cout << "aut1@" << aut1 << ": a=" << ap1a << " b=" << ap1b << '\n';
|
|
dict->dump(std::cout) << "---\n";
|
|
|
|
spot::twa_graph_ptr aut2 = spot::make_twa_graph(dict);
|
|
int ap2c = aut2->register_ap("c");
|
|
int ap2b = aut2->register_ap("b");
|
|
std::cout << "aut2@" << aut2 << ": b=" << ap2b << " c=" << ap2c << '\n';
|
|
dict->dump(std::cout) << "---\n";
|
|
|
|
aut1 = nullptr;
|
|
std::cout << "aut1 destroyed\n";
|
|
dict->dump(std::cout) << "---\n";
|
|
|
|
aut2 = nullptr;
|
|
std::cout << "aut2 destroyed\n";
|
|
dict->dump(std::cout);
|
|
}
|
|
#+END_SRC
|
|
|
|
#+RESULTS:
|
|
#+begin_example
|
|
aut1@0x55bff3d24340: a=0 b=1
|
|
Variable Map:
|
|
0 Var[a] x1 { 0x55bff3d24340 }
|
|
1 Var[b] x1 { 0x55bff3d24340 }
|
|
Anonymous lists:
|
|
[0]
|
|
Free list:
|
|
|
|
---
|
|
aut2@0x55bff3d258d0: b=1 c=2
|
|
Variable Map:
|
|
0 Var[a] x1 { 0x55bff3d24340 }
|
|
1 Var[b] x2 { 0x55bff3d24340 0x55bff3d258d0 }
|
|
2 Var[c] x1 { 0x55bff3d258d0 }
|
|
Anonymous lists:
|
|
[0]
|
|
Free list:
|
|
|
|
---
|
|
aut1 destroyed
|
|
Variable Map:
|
|
0 Free
|
|
1 Var[b] x1 { 0x55bff3d258d0 }
|
|
2 Var[c] x1 { 0x55bff3d258d0 }
|
|
Anonymous lists:
|
|
[0]
|
|
Free list:
|
|
(0, 1)
|
|
---
|
|
aut2 destroyed
|
|
Variable Map:
|
|
0 Free
|
|
1 Free
|
|
2 Free
|
|
Anonymous lists:
|
|
[0]
|
|
Free list:
|
|
(0, 3)
|
|
#+end_example
|
|
|
|
For each BDD variable registered to the =bdd_dict=, we have one line
|
|
that gives: the variable number, its meaning (e.g. =Var[b]=), its
|
|
registration count (=x2=), and a list of pointers to the objects that
|
|
registered the association.
|
|
|
|
Every time =twa::register_ap()= is called, it calls a similar function
|
|
in the =bdd_dict= to check for an existing association or register a
|
|
new one. When =aut1= is deleted, it unregisters all its variables,
|
|
causing variable =0= to become free. The free list is actually a list
|
|
of pairs representing ranges of free variables that can be reassigned
|
|
by the BDD dict when needed. (The *anonymous list* serves when
|
|
[[#anonymous][*anonymous BDD variables*]] are used.)
|
|
|
|
Such a low-level registration is usually handled by the following
|
|
interface:
|
|
|
|
#+BEGIN_SRC C++ :exports code
|
|
// return a BDD variable number for f
|
|
int bdd_dict::register_proposition(formula f, const void* for_me);
|
|
// release the BDD variable
|
|
void bdd_dict::unregister_variable(int var, const void* me);
|
|
// release all BDD variables registered by me
|
|
void bdd_dict::unregister_all_my_variables(const void* me);
|
|
// register the same variables as another object
|
|
void bdd_dict::register_all_variables_of(const void* from_other,
|
|
const void* for_me);
|
|
#+END_SRC
|
|
|
|
The last function may be a bit tricky to use, because we need to be
|
|
sure that another object has registered some variables. You can rely
|
|
on the fact that each =twa= automaton register its variables this way.
|
|
|
|
Now, in most cases, there is no need to worry about the =bdd_dict=.
|
|
Automata will register and unregister variables as needed. Other
|
|
objects like =spot::twa_word= will do the same.
|
|
|
|
There are at least two situations where one may need to deal with the
|
|
=bdd_dict=:
|
|
1. One case is when [[#ap][creating a derived object that store some BDD
|
|
representing a formula over atomic proposition]] (but without
|
|
reference to their original automaton).
|
|
2. Another case is when [[#anonymous][more BDD variables (maybe
|
|
unrelated to atomic propositions) are needed]].
|
|
|
|
These two cases are discussed in the next sections.
|
|
|
|
* Prolonging the association between a BDD variable and an atomic proposition
|
|
:PROPERTIES:
|
|
:CUSTOM_ID: ap
|
|
:END:
|
|
|
|
Let us implement an object representing a set of transitions of the
|
|
form $(src, guard, dest)$. This can for instance be used to store
|
|
all transition that belong to a certain acceptance set.
|
|
|
|
#+NAME: trans_set
|
|
#+begin_src python :export code
|
|
import spot
|
|
|
|
class trans_set:
|
|
def __init__(self, dict):
|
|
self.set = set()
|
|
self.dict = dict
|
|
def add_trans(self, src, guard, dst):
|
|
self.set.add((src, guard, dst))
|
|
def str_trans(self, src, guard, dst):
|
|
f = spot.bdd_format_formula(self.dict, guard)
|
|
return f"({src},{f},{dst})"
|
|
def __str__(self):
|
|
return '{' + ",".join([ self.str_trans(*t) for t in self.set ]) + '}'
|
|
|
|
def accepting_set(aut, num):
|
|
ts = trans_set(aut.get_dict())
|
|
for e in aut.edges():
|
|
if e.acc.has(num):
|
|
ts.add_trans(e.src, e.cond, e.dst)
|
|
return ts
|
|
#+end_src
|
|
|
|
The above code has two definitions.
|
|
1. The =trans_set= class is a set of transitions that can be printed.
|
|
It stores a =bdd_dict= so that it can print the guard of the
|
|
transition.
|
|
2. The =accepting_set= function iterates over an automaton, and saves
|
|
all transitions that belong to a given acceptance set number.
|
|
|
|
For instance, we can now translate an automaton, compute its acceptance
|
|
set 0, and print it as follows:
|
|
|
|
#+begin_src python :noweb strip-export
|
|
<<trans_set>>
|
|
aut = spot.translate('GF(a <-> XXa)')
|
|
ts = accepting_set(aut, 0)
|
|
print(ts)
|
|
#+end_src
|
|
#+RESULTS:
|
|
: {(0,a,3),(1,a,1),(2,!a,2),(3,!a,0)}
|
|
|
|
The code of =trans_set= is in fact bogus. The problem is that it
|
|
assumes the association between the atomic propositions and the BDD
|
|
variable is still available when the =str_trans= method is called.
|
|
However, that might not be the case.
|
|
|
|
The following call sequence demonstrates the problem:
|
|
|
|
#+begin_src python :noweb strip-export
|
|
<<trans_set>>
|
|
try:
|
|
ts = accepting_set(spot.translate('GF(a <-> XXa)'), 0)
|
|
print(ts)
|
|
except RuntimeError as e:
|
|
print("ERROR:", e)
|
|
#+end_src
|
|
|
|
#+RESULTS:
|
|
: ERROR: bdd_to_formula() was passed a bdd with a variable that is not in the dictionary
|
|
|
|
In this case, the temporary automaton constructed by
|
|
=spot.translate()= and passed to the =accepting_set()= function is
|
|
destroyed right after the =ts= object has been constructed. When the
|
|
automaton is destroyed, it removes all its associations from the
|
|
=bdd_dict=. This means that before the =print(ts)=, the dictionary
|
|
that was used by the automaton and that is still stored in the =ts=
|
|
objects is now empty. Consequently, calling =bdd_format_formula()=
|
|
raises an exception.
|
|
|
|
This can be fixed in a couple of ways. The easy way is to store the
|
|
automaton inside the =trans_set= object, to ensure that it will live
|
|
at least as long as the =trans_set= object. But maybe the automaton
|
|
is too big, and we really want to get rid of it? In this case
|
|
=trans_set= should tell the =bdd_dict= that it want to retain the
|
|
associations. The easiest way in this case is to call the
|
|
=register_all_variables_of()= method, because we know that each
|
|
automaton registers its variables.
|
|
|
|
#+begin_src python
|
|
import spot
|
|
|
|
class trans_set:
|
|
def __init__(self, aut):
|
|
self.set = set()
|
|
self.dict = aut.get_dict()
|
|
self.dict.register_all_variables_of(aut, self)
|
|
def __del__(self):
|
|
self.dict.unregister_all_my_variables(self)
|
|
def add_trans(self, src, guard, dest):
|
|
self.set.add((src, guard, dest))
|
|
def str_trans(self, src, guard, dest):
|
|
f = spot.bdd_format_formula(self.dict, guard)
|
|
return f"({src},{f},{dest})"
|
|
def __str__(self):
|
|
return '{' + ",".join([ self.str_trans(*t) for t in self.set ]) + '}'
|
|
|
|
def accepting_set(aut, num):
|
|
ts = trans_set(aut)
|
|
for e in aut.edges():
|
|
if e.acc.has(num):
|
|
ts.add_trans(e.src, e.cond, e.dst)
|
|
return ts
|
|
|
|
try:
|
|
ts = accepting_set(spot.translate('GF(a <-> XXa)'), 0)
|
|
print(ts)
|
|
except RuntimeError as e:
|
|
print("ERROR:", e)
|
|
#+end_src
|
|
|
|
#+RESULTS:
|
|
: {(0,a,3),(1,a,1),(2,!a,2),(3,!a,0)}
|
|
|
|
Notice that we have also added a destructor to =trans_set= to
|
|
unregister all the variables.
|
|
|
|
|
|
* Anonymous BDD variables
|
|
:PROPERTIES:
|
|
:CUSTOM_ID: anonymous
|
|
:END:
|
|
|
|
Another scenario where working with a =bdd_dict= is needed is when one
|
|
needs to allocate *anonymous BDD variables*. These are variables that
|
|
are not attached to any atomic proposition, and that can be used by
|
|
one algorithm privately. If multiple algorithms (or objects) register
|
|
anonymous variables, the =bdd_dict= will reuse anonymous variables
|
|
allocated to other algorithms. One can allocate multiple anonymous
|
|
variables with the following =bdd_dict= method:
|
|
|
|
#+begin_src c++ :export code
|
|
int bdd_dict::register_anonymous_variables(int n, const void* for_me);
|
|
#+end_src
|
|
|
|
A range of =n= variables will be allocated starting at the returned
|
|
index.
|
|
|
|
For instance, let's say that our =trans_set= should now store a
|
|
symbolic representation of a transition relation. For simplicity, we
|
|
assume we just want to store set of pairs =(src,dst)=: each pair will
|
|
be a conjunction $v_{src}\land v'_{dst}$ between two BDD variables
|
|
taken from two ranges ($v_i$ representing a source state $i$ and $v'i$
|
|
representing a destination state $i$), and the entire set will be a
|
|
disjunction of all these pairs. If the automaton has $n$ states, we
|
|
want to allocate $2n$ BDD variables for this purpose. We call these
|
|
variables *anonymous* because their meaning is unknown to the
|
|
=bdd_dict=.
|
|
|
|
#+begin_src python
|
|
import spot
|
|
from buddy import *
|
|
|
|
class trans_set:
|
|
def __init__(self, aut):
|
|
self.dict = d = aut.get_dict()
|
|
self.num_states = n = aut.num_states()
|
|
self.anonbase = b = d.register_anonymous_variables(2*n, self)
|
|
s = bddfalse
|
|
for e in aut.edges():
|
|
s |= self.src(e.src) & self.dst(e.dst)
|
|
self.rel = s
|
|
def src(self, n):
|
|
return bdd_ithvar(self.anonbase + n)
|
|
def dst(self, n):
|
|
return bdd_ithvar(self.anonbase + n + self.num_states)
|
|
def __del__(self):
|
|
self.dict.unregister_all_my_variables(self)
|
|
def __str__(self):
|
|
isop = spot.minato_isop(self.rel)
|
|
i = isop.next()
|
|
res = []
|
|
while i != bddfalse:
|
|
s = bdd_var(i) - self.anonbase
|
|
d = bdd_var(bdd_high(i)) - self.anonbase - self.num_states
|
|
res.append((s, d))
|
|
i = isop.next()
|
|
return str(res)
|
|
|
|
ts = trans_set(spot.translate('GF(a <-> XXa)'))
|
|
print(ts)
|
|
#+end_src
|
|
|
|
#+RESULTS:
|
|
: [(0, 2), (0, 3), (1, 0), (1, 1), (2, 2), (2, 3), (3, 0), (3, 1)]
|
|
|
|
# LocalWords: utf bdd html args BDDs DAGs twa SRC aut ap bff src
|
|
# LocalWords: unregisters unregister dest init dst str num cond GF
|
|
# LocalWords: noweb XXa RuntimeError del v'i anonbase bddfalse isop
|
|
# LocalWords: ithvar
|