zlktree: cleanup the interface, and add interactive ACD

* tests/python/_zlktree.ipynb: Remove and replace by...
* tests/python/zlktree.ipynb: ... this more documented notebook.
* tests/Makefile.am: Adjust.
* doc/org/tut.org, NEWS: Mention zlktree.ipynb.
* spot/twaalgos/zlktree.hh, spot/twaalgos/zlktree.cc,
python/spot/__init__.py: Cleanup interface, and add support for
interactive display.
This commit is contained in:
Alexandre Duret-Lutz 2021-09-03 22:20:20 +02:00
parent dc17762e14
commit 5c5790039b
8 changed files with 6754 additions and 6130 deletions

16
NEWS
View file

@ -239,16 +239,12 @@ New in spot 2.9.8.dev (not yet released)
Additionally, this function now returns the number of states that Additionally, this function now returns the number of states that
have been merged (and therefore removed from the automaton). have been merged (and therefore removed from the automaton).
- spot::zielonka_tree is a new class that can be constructed from - spot::zielonka_tree and spot::acd are new class that implement the
any acceptance condition to help paritizing it. Zielonka Tree and Alternatic Cycle Decomposition, based on a paper
spot::zielonka_tree_transform() will paritize an automaton using by Casares et al. (ICALP'21). Those structures can be used to
the Zielong Tree of its acceptance. Similarly, spot::acd class paritize any automaton, and more. The graphical rendering of ACD
implement the Alternating Cycle Decomposition of any automaton. in Jupyter notebooks is Spot's first interactive output. See
The spot::acd_transform() function uses it to paritize any https://spot.lrde.epita.fr/ipynb/zlktree.html for more.
automaton optimally. These two transformations are based on a
paper by Casares et al. (ICALP'21). The python bindings for
spot.zielonka_tree and spot.acd will display those structure
graphically, making it easier to explore those concepts.
Python: Python:

View file

@ -91,6 +91,7 @@ real notebooks instead.
- [[https://spot.lrde.epita.fr/ipynb/stutter-inv.html][=stutter-inv.ipynb=]] working with stutter-invariant formulas properties. - [[https://spot.lrde.epita.fr/ipynb/stutter-inv.html][=stutter-inv.ipynb=]] working with stutter-invariant formulas properties.
- [[https://spot.lrde.epita.fr/ipynb/satmin.html][=satmin.ipynb=]] Python interface for [[file:satmin.org][SAT-based minimization of deterministic ω-automata]]. - [[https://spot.lrde.epita.fr/ipynb/satmin.html][=satmin.ipynb=]] Python interface for [[file:satmin.org][SAT-based minimization of deterministic ω-automata]].
- [[https://spot.lrde.epita.fr/ipynb/twagraph-internals.html][=twagraph-internals.ipynb=]] Inner workings of the =twa_graph= class. - [[https://spot.lrde.epita.fr/ipynb/twagraph-internals.html][=twagraph-internals.ipynb=]] Inner workings of the =twa_graph= class.
- [[https://spot.lrde.epita.fr/ipynb/zlktree.html][=zlktree.ipynb=]] demonstration of Zielonka Trees and ACD
# LocalWords: utf html bdd IPython ipynb io randaut accparse acc # LocalWords: utf html bdd IPython ipynb io randaut accparse acc
# LocalWords: cond randltl genltl genaut scc testingaut ltsmin dve # LocalWords: cond randltl genltl genaut scc testingaut ltsmin dve

View file

@ -436,14 +436,81 @@ class zielonka_tree:
self.dot(ostr) self.dot(ostr)
return _ostream_to_svg(ostr) return _ostream_to_svg(ostr)
_acdnum = 0
@_extend(acd) @_extend(acd)
class acd: class acd:
def _repr_svg_(self): def _repr_svg_(self, id=None):
"""Output the ACD as SVG""" """Output the ACD as SVG"""
ostr = ostringstream() ostr = ostringstream()
self.dot(ostr) self.dot(ostr, id)
return _ostream_to_svg(ostr) return _ostream_to_svg(ostr)
def _repr_html_(self):
global _acdnum
num = _acdnum
_acdnum += 1
style = '''
.acdhigh ellipse,.acdacc ellipse,.acdacc path,.acdacc polygon{stroke:green;}
.acdhigh polygon,.acdrej ellipse,.acdrej path,.acdrej polygon{stroke:red;}
.acdbold ellipse,.acdbold polygon,.acdbold path{stroke-width:2;}
.acdrej polygon{fill:red;}
.acdacc polygon{fill:green;}
'''
js = '''
function acd{num}_clear(){{
$("#acd{num} .node,#acdaut{num} .node,#acdaut{num} .edge")
.removeClass("acdhigh acdbold acdacc acdrej");
}};
function acd{num}_state(state){{
acd{num}_clear();
$("#acd{num} .acdS" + state).addClass("acdhigh acdbold");
$("#acdaut{num} #S" + state).addClass("acdbold");
}};
function acd{num}_edge(edge){{
acd{num}_clear();
var theedge = $('#acdaut{num} #E' + edge)
var classList = theedge.attr('class').split(/\s+/);
$.each(classList, function(index, item) {{
if (item.startsWith('acdN')) {{
$("#acd{num} #" + item.substring(3)).addClass("acdhigh acdbold");
}}
}});
theedge.addClass("acdbold");
}};
function acd{num}_node(node, acc){{
acd{num}_clear();
$("#acdaut{num} .acdN" + node).addClass(acc
? "acdacc acdbold"
: "acdrej acdbold");
$("#acd{num} #N" + node).addClass("acdbold acdhigh");
}};'''.format(num=num)
me = 0
for n in range(self.node_count()):
for e in self.edges_of_node(n):
me = max(e, me)
js += '$("#acdaut{num} #E{e}").addClass("acdN{n}");'\
.format(num=num, e=e, n=n)
for e in range(1, me + 1):
js += '$("#acdaut{num} #E{e}")'\
'.click(function(){{acd{num}_edge({e});}});'\
.format(num=num, e=e)
for s in range(self.get_aut().num_states()):
js += '$("#acdaut{num} #S{s}")'\
'.click(function(){{acd{num}_state({s});}});'\
.format(num=num, s=s)
for n in range(self.node_count()):
v = int(self.node_acceptance(n))
js += '$("#acd{num} #N{n}")'\
'.click(function(){{acd{num}_node({n}, {v});}});'\
.format(num=num, n=n, v=v)
html = '<style>{}</style><div>{}</div><div>{}</div><script>{}</script>'\
.format(style,
self.get_aut().show('.i(acdaut{})'.format(num)).data,
self._repr_svg_("acd{}".format(num)),
js);
return html
def automata(*sources, timeout=None, ignore_abort=True, def automata(*sources, timeout=None, ignore_abort=True,
trust_hoa=True, no_sid=False, debug=False, trust_hoa=True, no_sid=False, debug=False,

View file

@ -22,6 +22,7 @@
#include <deque> #include <deque>
#include <spot/twaalgos/zlktree.hh> #include <spot/twaalgos/zlktree.hh>
#include <spot/twaalgos/genem.hh> #include <spot/twaalgos/genem.hh>
#include <spot/misc/escape.hh>
namespace spot namespace spot
{ {
@ -138,6 +139,9 @@ namespace spot
has_streett_shape_ = false; has_streett_shape_ = false;
} }
} }
bool empty_is_accepting = code.accepting(acc_cond::mark_t{});
empty_is_even_ = empty_is_accepting == is_even_;
} }
void zielonka_tree::dot(std::ostream& os) const void zielonka_tree::dot(std::ostream& os) const
@ -175,8 +179,10 @@ namespace spot
("zielonka_tree::step(): incorrect branch number"); ("zielonka_tree::step(): incorrect branch number");
if (!colors) if (!colors)
return {branch, nodes_[branch].level + 1}; {
unsigned lvl = nodes_[branch].level;
return {branch, lvl + ((lvl & 1) == empty_is_even_)};
}
unsigned child = 0; unsigned child = 0;
for (;;) for (;;)
{ {
@ -325,17 +331,6 @@ namespace spot
+ " is too large"); + " is too large");
} }
std::pair<unsigned, unsigned>
acd::step(unsigned branch, unsigned edge) const
{
if (SPOT_UNLIKELY(nodes_.size() < branch || nodes_[branch].first_child))
throw std::runtime_error
("acd::next_branch(): incorrect branch number");
// FIXME
(void)edge;
return {branch, 0};
}
acd::acd(const const_twa_graph_ptr& aut) acd::acd(const const_twa_graph_ptr& aut)
: si_(new scc_info(aut)), own_si_(true), trees_(si_->scc_count()) : si_(new scc_info(aut)), own_si_(true), trees_(si_->scc_count())
{ {
@ -485,7 +480,7 @@ namespace spot
} }
} }
unsigned acd::leftmost_branch_(unsigned n, unsigned state) unsigned acd::leftmost_branch_(unsigned n, unsigned state) const
{ {
loop: loop:
unsigned first_child = nodes_[n].first_child; unsigned first_child = nodes_[n].first_child;
@ -507,42 +502,45 @@ namespace spot
} }
unsigned acd::first_branch(unsigned s, unsigned scc) unsigned acd::first_branch(unsigned s) const
{ {
if (scc > trees_.size()) if (SPOT_UNLIKELY(aut_->num_states() < s))
report_invalid_scc_number(scc, "first_branch"); throw std::runtime_error("first_branch(): unknown state " +
std::to_string(s));
unsigned scc = si_->scc_of(s);
if (trees_[scc].trivial) // the branch is irrelevant for transiant SCCs if (trees_[scc].trivial) // the branch is irrelevant for transiant SCCs
return 0; return 0;
unsigned n = trees_[scc].root; unsigned n = trees_[scc].root;
if (SPOT_UNLIKELY(!nodes_[n].states[s])) assert(nodes_[n].states[s]);
throw std::runtime_error("first_branch(): state " +
std::to_string(s) + " not found in SCC " +
std::to_string(scc));
return leftmost_branch_(n, s); return leftmost_branch_(n, s);
} }
std::pair<unsigned, unsigned> std::pair<unsigned, unsigned>
acd::step(unsigned branch, unsigned edge) acd::step(unsigned branch, unsigned edge) const
{ {
if (SPOT_UNLIKELY(nodes_.size() < branch)) if (SPOT_UNLIKELY(nodes_.size() < branch))
throw std::runtime_error("acd::step(): incorrect branch number"); throw std::runtime_error("acd::step(): incorrect branch number");
if (SPOT_UNLIKELY(nodes_[branch].edges.size() < edge))
throw std::runtime_error("acd::step(): incorrect edge number");
unsigned child = 0; unsigned child = 0;
unsigned obranch = branch; unsigned dst = aut_->edge_storage(edge).dst;
while (!nodes_[branch].edges[edge]) while (!nodes_[branch].edges[edge])
{ {
unsigned parent = nodes_[branch].parent; unsigned parent = nodes_[branch].parent;
if (SPOT_UNLIKELY(branch == parent)) if (SPOT_UNLIKELY(branch == parent))
throw std::runtime_error("acd::step(): edge " + {
std::to_string(edge) + // We are changing SCC, so the level emitted does not
" is not on branch " + // matter.
std::to_string(obranch)); assert(si_->scc_of(aut_->edge_storage(edge).src)
!= si_->scc_of(dst));
return { first_branch(dst), 0 };
}
child = branch; child = branch;
branch = parent; branch = parent;
} }
unsigned lvl = nodes_[branch].level; unsigned lvl = nodes_[branch].level;
unsigned dst = aut_->edge_storage(edge).dst;
if (child != 0) if (child != 0)
{ {
unsigned start_child = child; unsigned start_child = child;
@ -562,10 +560,14 @@ namespace spot
} }
} }
void acd::dot(std::ostream& os) const void acd::dot(std::ostream& os, const char* id) const
{ {
os << "digraph acd {\n labelloc=\"t\"\n label=\"\n" os << "digraph acd {\n labelloc=\"t\"\n label=\"\n"
<< (is_even_ ? "min even\"" : "min odd\"\n"); << (is_even_ ? "min even\"" : "min odd\"\n");
if (id)
escape_str(os << " id=\"", id)
// fill the node so that the entire node is clickable
<< "\"\n node [id=\"N\\N\", style=filled, fillcolor=white]\n";
unsigned ns = nodes_.size(); unsigned ns = nodes_.size();
for (unsigned n = 0; n < ns; ++n) for (unsigned n = 0; n < ns; ++n)
{ {
@ -642,9 +644,22 @@ namespace spot
os << "\nlvl: " << nodes_[n].level; os << "\nlvl: " << nodes_[n].level;
if (!first_child) if (!first_child)
os << "\n<" << n << '>'; os << "\n<" << n << '>';
// use a fillcolor so that the entire node is clickable
os << "\", shape=" os << "\", shape="
<< (((nodes_[n].level & 1) ^ is_even_) ? "ellipse" : "box") << (((nodes_[n].level & 1) ^ is_even_) ? "ellipse" : "box");
<< "]\n"; if (id)
{
os << " class=\"";
const char* sep = "";
for (unsigned n = 0; n < nstates; ++n)
if (states[n] && si_->scc_of(n) == scc)
{
os << sep << "acdS" << n << '\n';
sep = " ";
}
os << '\"';
}
os << "]\n";
if (first_child) if (first_child)
{ {
unsigned child = first_child; unsigned child = first_child;
@ -659,6 +674,27 @@ namespace spot
os << "}\n"; os << "}\n";
} }
bool acd::node_acceptance(unsigned n) const
{
if (SPOT_UNLIKELY(nodes_.size() < n))
throw std::runtime_error("acd::node_acceptance(): unknown node");
return (nodes_[n].level & 1) ^ is_even_;
}
std::vector<unsigned> acd::edges_of_node(unsigned n) const
{
if (SPOT_UNLIKELY(nodes_.size() < n))
throw std::runtime_error("acd::edges_of_node(): unknown node");
std::vector<unsigned> res;
unsigned scc = nodes_[n].scc;
auto& edges = nodes_[n].edges;
unsigned nedges = edges.size();
for (unsigned e = 1; e < nedges; ++e)
if (edges[e] && si_->scc_of(aut_->edge_storage(e).dst) == scc)
res.push_back(e);
return res;
}
twa_graph_ptr twa_graph_ptr
acd_transform(const const_twa_graph_ptr& a, bool colored) acd_transform(const const_twa_graph_ptr& a, bool colored)
{ {
@ -714,7 +750,7 @@ namespace spot
}; };
unsigned init = a->get_init_state_number(); unsigned init = a->get_init_state_number();
zlk_state s(init, theacd.first_branch(init, si.scc_of(init))); zlk_state s(init, theacd.first_branch(init));
new_state(s); new_state(s);
unsigned max_color = 0; unsigned max_color = 0;
bool is_even = theacd.is_even(); bool is_even = theacd.is_even();
@ -734,7 +770,7 @@ namespace spot
unsigned dst_scc = si.scc_of(i.dst); unsigned dst_scc = si.scc_of(i.dst);
if (dst_scc != src_scc) if (dst_scc != src_scc)
{ {
newbranch = theacd.first_branch(i.dst, dst_scc); newbranch = theacd.first_branch(i.dst);
prio = 0; prio = 0;
} }
else else

View file

@ -71,10 +71,12 @@ namespace spot
/// definition since it allows a set of colors to be processed, /// definition since it allows a set of colors to be processed,
/// and not exactly one color. When multiple colors are given, /// and not exactly one color. When multiple colors are given,
/// the minimum corresponding level is returned. When no color is /// the minimum corresponding level is returned. When no color is
/// given, the branch is not changed and the level returned is one /// given, the branch is not changed and the level returned might
/// more than the depth of that branch (this is as if the tree add /// be one more than the depth of that branch (as if the tree had
/// another layer of leaves labeled by the empty sets, that do not /// another layer of leaves labeled by the empty sets, that we
/// store for simplicity). /// do not store). Whether the depth for no color is the depth
/// of the node or one more depend on whether the absence of
/// color had the same parity has the current node.
std::pair<unsigned, unsigned> std::pair<unsigned, unsigned>
step(unsigned branch, acc_cond::mark_t colors) const; step(unsigned branch, acc_cond::mark_t colors) const;
@ -127,6 +129,7 @@ namespace spot
unsigned one_branch_ = 0; unsigned one_branch_ = 0;
unsigned num_branches_ = 0; unsigned num_branches_ = 0;
bool is_even_; bool is_even_;
bool empty_is_even_;
bool has_rabin_shape_ = true; bool has_rabin_shape_ = true;
bool has_streett_shape_ = true; bool has_streett_shape_ = true;
}; };
@ -164,19 +167,34 @@ namespace spot
~acd(); ~acd();
/// \brief Walk through the ACD. /// \brief Step through the ACD.
/// ///
/// Given a \a branch number, and an edge, this returns /// Given a \a branch number, and an edge, this returns
/// a pair (new branch, level), as needed in definition 4.6 of /// a pair (new branch, level), as needed in definition 4.6 of
/// \cite casares.21.icalp (or definition 4.20 in the full version). /// \cite casares.21.icalp (or definition 4.20 in the full version).
/// We do not have to specify any SCC, because the branch number are
/// different in each SCC.
/// ///
/// The level correspond to the priority of a minimum parity acceptance /// The level correspond to the priority of a minimum parity acceptance
/// condition, with the parity odd/even as specified by is_even(). /// condition, with the parity odd/even as specified by is_even().
std::pair<unsigned, unsigned> std::pair<unsigned, unsigned>
step(unsigned branch, unsigned edge) const; step(unsigned branch, unsigned edge) const;
/// \brief Return the list of edges covered by node n of the ACD.
///
/// This is mostly used for interactive display.
std::vector<unsigned> edges_of_node(unsigned n) const;
/// \brief Return the number of nodes in the the ACD forest.
unsigned node_count() const
{
return nodes_.size();
}
/// Tell whether a node store accepting or rejecting cycles.
///
/// This is mostly used for interactive display.
bool node_acceptance(unsigned n) const;
/// \brief Whether the ACD corresponds to a min even or min odd /// \brief Whether the ACD corresponds to a min even or min odd
/// parity acceptance in SCC \a scc. /// parity acceptance in SCC \a scc.
bool is_even(unsigned scc) const bool is_even(unsigned scc) const
@ -198,20 +216,9 @@ namespace spot
} }
/// \brief Return the first branch for \a state /// \brief Return the first branch for \a state
/// unsigned first_branch(unsigned state) const;
/// \a scc should correspond to the SCC containing \a state.
/// (this class does not store the scc_info passed at construction)
unsigned first_branch(unsigned state, unsigned scc);
/// \brief Step into the ACD unsigned scc_max_level(unsigned scc) const
///
/// Given an edge \a edge on branch \a branch,
/// return a pair (new branch, level) giving the proirity (\a level) to
/// emit, and the branch of the destination state.
std::pair<unsigned, unsigned>
step(unsigned branch, unsigned edge);
unsigned scc_max_level(unsigned scc)
{ {
if (scc >= scc_count_) if (scc >= scc_count_)
report_invalid_scc_number(scc, "scc_max_level"); report_invalid_scc_number(scc, "scc_max_level");
@ -244,8 +251,17 @@ namespace spot
return has_streett_shape() && has_rabin_shape(); return has_streett_shape() && has_rabin_shape();
} }
/// \brief Return the automaton on which the ACD is defined.
const const_twa_graph_ptr get_aut() const
{
return aut_;
}
/// \brief Render the ACD as in GraphViz format. /// \brief Render the ACD as in GraphViz format.
void dot(std::ostream&) const; ///
/// If \a id is given, it is used to give the graph an id, and,
/// all nodes will get ids as well.
void dot(std::ostream&, const char* id = nullptr) const;
private: private:
const scc_info* si_; const scc_info* si_;
@ -312,7 +328,7 @@ namespace spot
void build_(); void build_();
// leftmost branch of \a node that contains \a state // leftmost branch of \a node that contains \a state
unsigned leftmost_branch_(unsigned node, unsigned state); unsigned leftmost_branch_(unsigned node, unsigned state) const;
#ifndef SWIG #ifndef SWIG
[[noreturn]] static [[noreturn]] static

View file

@ -449,7 +449,7 @@ TESTS_python = \
python/twagraph.py \ python/twagraph.py \
python/toweak.py \ python/toweak.py \
python/_word.ipynb \ python/_word.ipynb \
python/_zlktree.ipynb \ python/zlktree.ipynb \
$(TESTS_ipython) $(TESTS_ipython)
endif endif

File diff suppressed because it is too large Load diff

6570
tests/python/zlktree.ipynb Normal file

File diff suppressed because one or more lines are too long