degen: detect superfluous SCCs and remove them
Suggested by Maximilien Colange. * spot/twaalgos/degen.cc: If the output has more SCC than the input, detect useless SCCs and remove them. * spot/twaalgos/postproc.cc, spot/twaalgos/postproc.hh, spot/twaalgos/degen.hh: Add support for a degen-remscc option. * bin/spot-x.cc, NEWS: Document it. * tests/core/degenscc.test: New file. * tests/Makefile.am: Add it. * tests/core/det.test: Lower some expected size (yay!).
This commit is contained in:
parent
ce5e3b654f
commit
900b344c9a
9 changed files with 147 additions and 22 deletions
8
NEWS
8
NEWS
|
|
@ -96,6 +96,14 @@ New in spot 2.4.0.dev (not yet released)
|
||||||
transitions on the fly from an alternating Büchi automaton
|
transitions on the fly from an alternating Büchi automaton
|
||||||
using Miyano and Hayashi's breakpoint algorithm.
|
using Miyano and Hayashi's breakpoint algorithm.
|
||||||
|
|
||||||
|
- In some cases, spot::degeneralize() would output Büchi automata
|
||||||
|
with more SCCs than its input. This was hard to notice, because
|
||||||
|
very often simulation-based simplifications remove those extra
|
||||||
|
SCCs. This situation is now detected by spot::degeneralized() and
|
||||||
|
fixed before returning the automaton. A new optionnal argument
|
||||||
|
can be passed to disable this behavior (or use -x degen-remscc=0
|
||||||
|
from the command-line).
|
||||||
|
|
||||||
Deprecation notices:
|
Deprecation notices:
|
||||||
|
|
||||||
(These functions still work but compilers emit warnings.)
|
(These functions still work but compilers emit warnings.)
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,9 @@ has an accepting self-loop, then level L is replaced by the accepting \
|
||||||
level, as it might favor finding accepting cycles earlier. If \
|
level, as it might favor finding accepting cycles earlier. If \
|
||||||
degen-lowinit is non-zero, then level L is always used without looking \
|
degen-lowinit is non-zero, then level L is always used without looking \
|
||||||
for the presence of an accepting self-loop.") },
|
for the presence of an accepting self-loop.") },
|
||||||
|
{ DOC("degen-remscc", "If non-zero (the default), make sure the output \
|
||||||
|
of the degenalization has as many SCCs as the input, by removing superfluous \
|
||||||
|
ones.") },
|
||||||
{ DOC("det-scc", "Set to 0 to disable scc-based optimizations in \
|
{ DOC("det-scc", "Set to 0 to disable scc-based optimizations in \
|
||||||
the determinization algorithm.") },
|
the determinization algorithm.") },
|
||||||
{ DOC("det-simul", "Set to 0 to disable simulation-based optimizations in \
|
{ DOC("det-simul", "Set to 0 to disable simulation-based optimizations in \
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
#include <spot/twaalgos/sccinfo.hh>
|
#include <spot/twaalgos/sccinfo.hh>
|
||||||
#include <spot/twa/bddprint.hh>
|
#include <spot/twa/bddprint.hh>
|
||||||
|
|
||||||
|
|
@ -240,7 +241,8 @@ namespace spot
|
||||||
twa_graph_ptr
|
twa_graph_ptr
|
||||||
degeneralize_aux(const const_twa_graph_ptr& a, bool use_z_lvl,
|
degeneralize_aux(const const_twa_graph_ptr& a, bool use_z_lvl,
|
||||||
bool use_cust_acc_orders, int use_lvl_cache,
|
bool use_cust_acc_orders, int use_lvl_cache,
|
||||||
bool skip_levels, bool ignaccsl)
|
bool skip_levels, bool ignaccsl,
|
||||||
|
bool remove_extra_scc)
|
||||||
{
|
{
|
||||||
if (!a->acc().is_generalized_buchi())
|
if (!a->acc().is_generalized_buchi())
|
||||||
throw std::runtime_error
|
throw std::runtime_error
|
||||||
|
|
@ -249,7 +251,10 @@ namespace spot
|
||||||
throw std::runtime_error
|
throw std::runtime_error
|
||||||
("degeneralize() does not support alternation");
|
("degeneralize() does not support alternation");
|
||||||
|
|
||||||
bool use_scc = use_lvl_cache || use_cust_acc_orders || use_z_lvl;
|
bool use_scc = (use_lvl_cache
|
||||||
|
|| use_cust_acc_orders
|
||||||
|
|| use_z_lvl
|
||||||
|
|| remove_extra_scc);
|
||||||
|
|
||||||
bdd_dict_ptr dict = a->get_dict();
|
bdd_dict_ptr dict = a->get_dict();
|
||||||
|
|
||||||
|
|
@ -305,12 +310,11 @@ namespace spot
|
||||||
std::vector<std::pair<unsigned, bool>> lvl_cache(a->num_states());
|
std::vector<std::pair<unsigned, bool>> lvl_cache(a->num_states());
|
||||||
|
|
||||||
// Compute SCCs in order to use any optimization.
|
// Compute SCCs in order to use any optimization.
|
||||||
scc_info* m = nullptr;
|
std::unique_ptr<scc_info> m = use_scc ?
|
||||||
if (use_scc)
|
std::make_unique<scc_info>(a) : nullptr;
|
||||||
m = new scc_info(a);
|
|
||||||
|
|
||||||
// Cache for common outgoing/incoming acceptances.
|
// Cache for common outgoing/incoming acceptances.
|
||||||
inout_acc inout(a, m);
|
inout_acc inout(a, m.get());
|
||||||
|
|
||||||
queue_t todo;
|
queue_t todo;
|
||||||
|
|
||||||
|
|
@ -337,7 +341,7 @@ namespace spot
|
||||||
if (!skip_levels)
|
if (!skip_levels)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// There is not accepting level for TBA, let reuse level 0.
|
// There is no accepting level for TBA, let reuse level 0.
|
||||||
if (!want_sba && s.second == order.size())
|
if (!want_sba && s.second == order.size())
|
||||||
s.second = 0;
|
s.second = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -373,9 +377,8 @@ namespace spot
|
||||||
|
|
||||||
// Level cache stores one encountered level for each state
|
// Level cache stores one encountered level for each state
|
||||||
// (the value of use_lvl_cache determinates which level
|
// (the value of use_lvl_cache determinates which level
|
||||||
// should be remembered).
|
// should be remembered). This cache is used when
|
||||||
// When entering an SCC first the lvl_cache is checked.
|
// re-entering the SCC.
|
||||||
// If such state exists level from chache is used.
|
|
||||||
if (use_lvl_cache)
|
if (use_lvl_cache)
|
||||||
{
|
{
|
||||||
unsigned lvl = ds.second;
|
unsigned lvl = ds.second;
|
||||||
|
|
@ -612,10 +615,49 @@ namespace spot
|
||||||
std::cout << '\n';
|
std::cout << '\n';
|
||||||
orders.print();
|
orders.print();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
delete m;
|
|
||||||
|
|
||||||
res->merge_edges();
|
res->merge_edges();
|
||||||
|
|
||||||
|
unsigned res_ns = res->num_states();
|
||||||
|
if (!remove_extra_scc || res_ns <= a->num_states())
|
||||||
|
return res;
|
||||||
|
|
||||||
|
scc_info si_res(res);
|
||||||
|
unsigned res_scc_count = si_res.scc_count();
|
||||||
|
if (res_scc_count <= m->scc_count())
|
||||||
|
return res;
|
||||||
|
|
||||||
|
// If we reach this place, we have more SCCs in the output than
|
||||||
|
// in the input. This means that we have created some redundant
|
||||||
|
// SCCs. Often, these are trivial SCCs created in front of
|
||||||
|
// their larger sisters, because we did not pick the correct
|
||||||
|
// level when entering the SCC for the first time, and the level
|
||||||
|
// we picked has not been seen again when exploring the SCC.
|
||||||
|
// But it could also be the case that by entering the SCC in two
|
||||||
|
// different ways, we create two clones of the SCC (I haven't
|
||||||
|
// encountered any such case, but I do not want to rule it out
|
||||||
|
// in the code below).
|
||||||
|
//
|
||||||
|
// Now we will iterate over the SCCs in topological order to
|
||||||
|
// remember the "bottomost" SCCs that contain each original
|
||||||
|
// state. If an original state is duplicated in a higher SCC,
|
||||||
|
// it can be shunted away. Amen.
|
||||||
|
std::vector<unsigned> bottomost_occurence(a->num_states());
|
||||||
|
{
|
||||||
|
unsigned n = res_scc_count;
|
||||||
|
do
|
||||||
|
for (unsigned s: si_res.states_of(--n))
|
||||||
|
bottomost_occurence[(*orig_states)[s]] = s;
|
||||||
|
while (n);
|
||||||
|
}
|
||||||
|
std::vector<unsigned> retarget(res_ns);
|
||||||
|
for (unsigned n = 0; n < res_ns; ++n)
|
||||||
|
{
|
||||||
|
unsigned other = bottomost_occurence[(*orig_states)[n]];
|
||||||
|
retarget[n] = (si_res.scc_of(n) != si_res.scc_of(other)) ? other : n;
|
||||||
|
}
|
||||||
|
for (auto& e: res->edges())
|
||||||
|
e.dst = retarget[e.dst];
|
||||||
|
res->purge_unreachable_states();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -623,7 +665,8 @@ namespace spot
|
||||||
twa_graph_ptr
|
twa_graph_ptr
|
||||||
degeneralize(const const_twa_graph_ptr& a,
|
degeneralize(const const_twa_graph_ptr& a,
|
||||||
bool use_z_lvl, bool use_cust_acc_orders,
|
bool use_z_lvl, bool use_cust_acc_orders,
|
||||||
int use_lvl_cache, bool skip_levels, bool ignaccsl)
|
int use_lvl_cache, bool skip_levels, bool ignaccsl,
|
||||||
|
bool remove_extra_scc)
|
||||||
{
|
{
|
||||||
// If this already a degeneralized digraph, there is nothing we
|
// If this already a degeneralized digraph, there is nothing we
|
||||||
// can improve.
|
// can improve.
|
||||||
|
|
@ -631,13 +674,15 @@ namespace spot
|
||||||
return std::const_pointer_cast<twa_graph>(a);
|
return std::const_pointer_cast<twa_graph>(a);
|
||||||
|
|
||||||
return degeneralize_aux<true>(a, use_z_lvl, use_cust_acc_orders,
|
return degeneralize_aux<true>(a, use_z_lvl, use_cust_acc_orders,
|
||||||
use_lvl_cache, skip_levels, ignaccsl);
|
use_lvl_cache, skip_levels, ignaccsl,
|
||||||
|
remove_extra_scc);
|
||||||
}
|
}
|
||||||
|
|
||||||
twa_graph_ptr
|
twa_graph_ptr
|
||||||
degeneralize_tba(const const_twa_graph_ptr& a,
|
degeneralize_tba(const const_twa_graph_ptr& a,
|
||||||
bool use_z_lvl, bool use_cust_acc_orders,
|
bool use_z_lvl, bool use_cust_acc_orders,
|
||||||
int use_lvl_cache, bool skip_levels, bool ignaccsl)
|
int use_lvl_cache, bool skip_levels, bool ignaccsl,
|
||||||
|
bool remove_extra_scc)
|
||||||
{
|
{
|
||||||
// If this already a degeneralized digraph, there is nothing we
|
// If this already a degeneralized digraph, there is nothing we
|
||||||
// can improve.
|
// can improve.
|
||||||
|
|
@ -645,6 +690,7 @@ namespace spot
|
||||||
return std::const_pointer_cast<twa_graph>(a);
|
return std::const_pointer_cast<twa_graph>(a);
|
||||||
|
|
||||||
return degeneralize_aux<false>(a, use_z_lvl, use_cust_acc_orders,
|
return degeneralize_aux<false>(a, use_z_lvl, use_cust_acc_orders,
|
||||||
use_lvl_cache, skip_levels, ignaccsl);
|
use_lvl_cache, skip_levels, ignaccsl,
|
||||||
|
remove_extra_scc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ namespace spot
|
||||||
/// smallest number, 3 to keep the largest level, and 1 to keep the
|
/// smallest number, 3 to keep the largest level, and 1 to keep the
|
||||||
/// first level found). If \a ignaccsl is set, we do not directly
|
/// first level found). If \a ignaccsl is set, we do not directly
|
||||||
/// jump to the accepting level if the entering state has an
|
/// jump to the accepting level if the entering state has an
|
||||||
/// accepting self-loop.
|
/// accepting self-loop. If \a remove_extra_scc is set (the default)
|
||||||
|
/// we ensure that the output automaton has as many SCCs as the input
|
||||||
|
/// by removing superfluous SCCs.
|
||||||
///
|
///
|
||||||
/// Any of these three options will cause the SCCs of the automaton
|
/// Any of these three options will cause the SCCs of the automaton
|
||||||
/// \a a to be computed prior to its actual degeneralization.
|
/// \a a to be computed prior to its actual degeneralization.
|
||||||
|
|
@ -65,13 +67,15 @@ namespace spot
|
||||||
bool use_cust_acc_orders = false,
|
bool use_cust_acc_orders = false,
|
||||||
int use_lvl_cache = 1,
|
int use_lvl_cache = 1,
|
||||||
bool skip_levels = true,
|
bool skip_levels = true,
|
||||||
bool ignaccsl = false);
|
bool ignaccsl = false,
|
||||||
|
bool remove_extra_scc = true);
|
||||||
|
|
||||||
SPOT_API twa_graph_ptr
|
SPOT_API twa_graph_ptr
|
||||||
degeneralize_tba(const const_twa_graph_ptr& a, bool use_z_lvl = true,
|
degeneralize_tba(const const_twa_graph_ptr& a, bool use_z_lvl = true,
|
||||||
bool use_cust_acc_orders = false,
|
bool use_cust_acc_orders = false,
|
||||||
int use_lvl_cache = 1,
|
int use_lvl_cache = 1,
|
||||||
bool skip_levels = true,
|
bool skip_levels = true,
|
||||||
bool ignaccsl = false);
|
bool ignaccsl = false,
|
||||||
|
bool remove_extra_scc = true);
|
||||||
/// \@}
|
/// \@}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ namespace spot
|
||||||
degen_cache_ = opt->get("degen-lcache", 1);
|
degen_cache_ = opt->get("degen-lcache", 1);
|
||||||
degen_lskip_ = opt->get("degen-lskip", 1);
|
degen_lskip_ = opt->get("degen-lskip", 1);
|
||||||
degen_lowinit_ = opt->get("degen-lowinit", 0);
|
degen_lowinit_ = opt->get("degen-lowinit", 0);
|
||||||
|
degen_remscc_ = opt->get("degen-remscc", 1);
|
||||||
det_scc_ = opt->get("det-scc", 1);
|
det_scc_ = opt->get("det-scc", 1);
|
||||||
det_simul_ = opt->get("det-simul", 1);
|
det_simul_ = opt->get("det-simul", 1);
|
||||||
det_stutter_ = opt->get("det-stutter", 1);
|
det_stutter_ = opt->get("det-stutter", 1);
|
||||||
|
|
@ -142,7 +143,7 @@ namespace spot
|
||||||
auto d = degeneralize(a,
|
auto d = degeneralize(a,
|
||||||
degen_reset_, degen_order_,
|
degen_reset_, degen_order_,
|
||||||
degen_cache_, degen_lskip_,
|
degen_cache_, degen_lskip_,
|
||||||
degen_lowinit_);
|
degen_lowinit_, degen_remscc_);
|
||||||
return do_sba_simul(d, ba_simul_);
|
return do_sba_simul(d, ba_simul_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,7 @@ namespace spot
|
||||||
int degen_cache_ = 1;
|
int degen_cache_ = 1;
|
||||||
bool degen_lskip_ = true;
|
bool degen_lskip_ = true;
|
||||||
bool degen_lowinit_ = false;
|
bool degen_lowinit_ = false;
|
||||||
|
bool degen_remscc_ = true;
|
||||||
bool det_scc_ = true;
|
bool det_scc_ = true;
|
||||||
bool det_simul_ = true;
|
bool det_simul_ = true;
|
||||||
bool det_stutter_ = true;
|
bool det_stutter_ = true;
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,7 @@ TESTS_twa = \
|
||||||
core/degendet.test \
|
core/degendet.test \
|
||||||
core/degenid.test \
|
core/degenid.test \
|
||||||
core/degenlskip.test \
|
core/degenlskip.test \
|
||||||
|
core/degenscc.test \
|
||||||
core/randomize.test \
|
core/randomize.test \
|
||||||
core/lbttparse.test \
|
core/lbttparse.test \
|
||||||
core/scc.test \
|
core/scc.test \
|
||||||
|
|
|
||||||
61
tests/core/degenscc.test
Normal file
61
tests/core/degenscc.test
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2017 Laboratoire de Recherche et Développement de
|
||||||
|
# l'Epita (LRDE).
|
||||||
|
#
|
||||||
|
# This file is part of Spot, a model checking library.
|
||||||
|
#
|
||||||
|
# Spot is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Spot is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||||
|
# License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
. ./defs
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# This make sure that the degeneralize fonction does not create
|
||||||
|
# new SCCs.
|
||||||
|
#
|
||||||
|
# The following cases were found with
|
||||||
|
#
|
||||||
|
# % randltl -n -1 3 | ltl2tgba | autfilt --acc-sets=3.. |
|
||||||
|
# autfilt -B --stats='%C,%c,%M' | awk -F, '{ if ($1 < $2) { print $0; } }'
|
||||||
|
#
|
||||||
|
# before patching degeneralize, but today replacing -B by -Bx'!degen-remscc'
|
||||||
|
# should do the same.
|
||||||
|
cat >input <<EOF
|
||||||
|
p2 & GF(G(p0 & !p1) | (F!p0 & Fp1))
|
||||||
|
GF((Fp2 & X((p0 & Gp1) | (!p0 & F!p1))) | (G!p2 & X((p0 & F!p1) | (!p0 & Gp1))))
|
||||||
|
GF(p0 | GF((p2 M p1) | (Fp1 & F!p0) | G(p0 & !p1)))
|
||||||
|
((Gp2&FG((Gp1&Xp0)|(F!p1&X!p0)))|(F!p2 & GF((Gp1 & X!p0)|(Xp0 & F!p1)))) W !p0
|
||||||
|
G(((Fp1 & (p1 W Fp0)) | (G!p1 & (!p1 M G!p0))) M FG!p2)
|
||||||
|
Xp0 R F((p0 & FG(Gp2 U p1)) | (!p0 & GF(F!p2 R !p1)))
|
||||||
|
GF(p0 & (((p1 & Fp2) | (!p1 & G!p2)) M Gp1))
|
||||||
|
G(!p1 | (!p2 & F!p1) | (GFp2 U p0))
|
||||||
|
X(p1 | GF((Fp2 & F!p1) | G(p1 & !p2)))
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# We want to make sure the degeneralized automaton as less SCCs
|
||||||
|
# (it can be less if the simulation on the BA is lukier than on the TGBA)
|
||||||
|
ltl2tgba < input | autfilt -B --stats=": '%M'; test %C -ge %c" > test.sh
|
||||||
|
sh -x -e test.sh
|
||||||
|
|
||||||
|
# Make sur that this degen-remscc optimizition is actually doing something.
|
||||||
|
# The following test could fail in the future if we improve the translation
|
||||||
|
# of some of these formulas. In that case, regenerate the list of test
|
||||||
|
# formula using the command displayed above.
|
||||||
|
ltl2tgba < input |
|
||||||
|
autfilt -Bx'!degen-remscc' --stats=": '%M'; test %C -lt %c" > test.sh
|
||||||
|
sh -x -e test.sh
|
||||||
|
|
||||||
|
# We also want to make sure those degeneralized automata are correct
|
||||||
|
ltlcross -F input ltl2tgba 'ltl2tgba -B' 'ltl2tgba %f | autfilt -B > %O'
|
||||||
|
|
@ -41,7 +41,7 @@ cat >formulas <<'EOF'
|
||||||
1,6,GF!a U Xa
|
1,6,GF!a U Xa
|
||||||
1,5,(a | G(a M !b)) W Fc
|
1,5,(a | G(a M !b)) W Fc
|
||||||
1,6,Fa W Xb
|
1,6,Fa W Xb
|
||||||
1,10,X(a R ((!b & F!c) M X!a))
|
1,9,X(a R ((!b & F!c) M X!a))
|
||||||
1,2,XG!a R Fb
|
1,2,XG!a R Fb
|
||||||
1,4,GFc | (a & Fb)
|
1,4,GFc | (a & Fb)
|
||||||
1,6,X(a R (Fb R F!b))
|
1,6,X(a R (Fb R F!b))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue