new partial_degeneralize() algorithm
* spot/twaalgos/degen.hh, spot/twaalgos/degen.cc: Implement it. * tests/python/pdegen.py: New file. * tests/Makefile.am: Add it. * NEWS: Mention the new function.
This commit is contained in:
parent
894fda21f9
commit
bf7ccfe7e6
5 changed files with 372 additions and 45 deletions
4
NEWS
4
NEWS
|
|
@ -54,6 +54,10 @@ New in spot 2.8.4.dev (not yet released)
|
||||||
parity_min_odd(n) = parity_min(true, n)
|
parity_min_odd(n) = parity_min(true, n)
|
||||||
parity_min_even(n) = parity_min(false, n)
|
parity_min_even(n) = parity_min(false, n)
|
||||||
|
|
||||||
|
- partial_degeneralize() is a new function performing partial
|
||||||
|
degeneralization to get rid of conjunction of Inf terms in
|
||||||
|
acceptance conditions.
|
||||||
|
|
||||||
New in spot 2.8.4 (2019-12-08)
|
New in spot 2.8.4 (2019-12-08)
|
||||||
|
|
||||||
Bugs fixed:
|
Bugs fixed:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// -*- coding: utf-8 -*-
|
// -*- coding: utf-8 -*-
|
||||||
// Copyright (C) 2012-2018 Laboratoire de Recherche
|
// Copyright (C) 2012-2019 Laboratoire de Recherche
|
||||||
// et Développement de l'Epita (LRDE).
|
// et Développement de l'Epita (LRDE).
|
||||||
//
|
//
|
||||||
// This file is part of Spot, a model checking library.
|
// This file is part of Spot, a model checking library.
|
||||||
|
|
@ -237,6 +237,62 @@ namespace spot
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static void
|
||||||
|
keep_bottommost_copies(twa_graph_ptr& res,
|
||||||
|
scc_info& si_orig,
|
||||||
|
std::vector<unsigned>* orig_states)
|
||||||
|
{
|
||||||
|
unsigned res_ns = res->num_states();
|
||||||
|
auto a = si_orig.get_aut();
|
||||||
|
if (res_ns <= a->num_states())
|
||||||
|
return;
|
||||||
|
|
||||||
|
scc_info si_res(res, scc_info_options::TRACK_STATES);
|
||||||
|
unsigned res_scc_count = si_res.scc_count();
|
||||||
|
if (res_scc_count <= si_orig.scc_count())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 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 "bottommost" 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>
|
||||||
|
bottommost_occurence(a->num_states());
|
||||||
|
{
|
||||||
|
unsigned n = res_scc_count;
|
||||||
|
do
|
||||||
|
for (unsigned s: si_res.states_of(--n))
|
||||||
|
bottommost_occurence[(*orig_states)[s]] = s;
|
||||||
|
while (n);
|
||||||
|
}
|
||||||
|
std::vector<unsigned> retarget(res_ns);
|
||||||
|
for (unsigned n = 0; n < res_ns; ++n)
|
||||||
|
{
|
||||||
|
unsigned other = bottommost_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->set_init_state(retarget[res->get_init_state_number()]);
|
||||||
|
res->purge_unreachable_states();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template<bool want_sba>
|
template<bool want_sba>
|
||||||
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,
|
||||||
|
|
@ -617,48 +673,8 @@ namespace spot
|
||||||
orders.print();
|
orders.print();
|
||||||
#endif
|
#endif
|
||||||
res->merge_edges();
|
res->merge_edges();
|
||||||
|
if (remove_extra_scc)
|
||||||
unsigned res_ns = res->num_states();
|
keep_bottommost_copies(res, *(m.get()), orig_states);
|
||||||
if (!remove_extra_scc || res_ns <= a->num_states())
|
|
||||||
return res;
|
|
||||||
|
|
||||||
scc_info si_res(res, scc_info_options::TRACK_STATES);
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -694,4 +710,189 @@ namespace spot
|
||||||
use_lvl_cache, skip_levels, ignaccsl,
|
use_lvl_cache, skip_levels, ignaccsl,
|
||||||
remove_extra_scc);
|
remove_extra_scc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static acc_cond::mark_t
|
||||||
|
to_strip(const acc_cond::acc_code& code, acc_cond::mark_t todegen)
|
||||||
|
{
|
||||||
|
if (code.empty())
|
||||||
|
return todegen;
|
||||||
|
|
||||||
|
acc_cond::mark_t tostrip = todegen;
|
||||||
|
unsigned pos = code.size();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
switch (code[pos - 1].sub.op)
|
||||||
|
{
|
||||||
|
case acc_cond::acc_op::And:
|
||||||
|
case acc_cond::acc_op::Or:
|
||||||
|
--pos;
|
||||||
|
break;
|
||||||
|
case acc_cond::acc_op::Inf:
|
||||||
|
case acc_cond::acc_op::InfNeg:
|
||||||
|
pos -= 2;
|
||||||
|
if (code[pos].mark != todegen)
|
||||||
|
tostrip -= code[pos].mark;
|
||||||
|
break;
|
||||||
|
case acc_cond::acc_op::Fin:
|
||||||
|
case acc_cond::acc_op::FinNeg:
|
||||||
|
pos -= 2;
|
||||||
|
tostrip -= code[pos].mark;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (pos > 0);
|
||||||
|
return tostrip;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
update_acc_for_partial_degen(acc_cond::acc_code& code,
|
||||||
|
acc_cond::mark_t todegen,
|
||||||
|
acc_cond::mark_t tostrip,
|
||||||
|
acc_cond::mark_t accmark)
|
||||||
|
{
|
||||||
|
if (!todegen)
|
||||||
|
{
|
||||||
|
code &= acc_cond::acc_code::inf(accmark);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
unsigned pos = code.size();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
switch (code[pos - 1].sub.op)
|
||||||
|
{
|
||||||
|
case acc_cond::acc_op::And:
|
||||||
|
case acc_cond::acc_op::Or:
|
||||||
|
--pos;
|
||||||
|
break;
|
||||||
|
case acc_cond::acc_op::Inf:
|
||||||
|
case acc_cond::acc_op::InfNeg:
|
||||||
|
pos -= 2;
|
||||||
|
if (code[pos].mark == todegen)
|
||||||
|
code[pos].mark = accmark;
|
||||||
|
else
|
||||||
|
code[pos].mark = code[pos].mark.strip(tostrip);
|
||||||
|
break;
|
||||||
|
case acc_cond::acc_op::Fin:
|
||||||
|
case acc_cond::acc_op::FinNeg:
|
||||||
|
pos -= 2;
|
||||||
|
code[pos].mark = code[pos].mark.strip(tostrip);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (pos > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
twa_graph_ptr
|
||||||
|
partial_degeneralize(const const_twa_graph_ptr& a,
|
||||||
|
acc_cond::mark_t todegen)
|
||||||
|
{
|
||||||
|
auto res = make_twa_graph(a->get_dict());
|
||||||
|
res->copy_ap_of(a);
|
||||||
|
acc_cond::acc_code acc = a->get_acceptance();
|
||||||
|
|
||||||
|
acc_cond::mark_t tostrip = to_strip(acc, todegen);
|
||||||
|
acc_cond::mark_t keep = a->acc().all_sets() - tostrip;
|
||||||
|
acc_cond::mark_t degenmark = {keep.count()};
|
||||||
|
|
||||||
|
update_acc_for_partial_degen(acc, todegen, tostrip, degenmark);
|
||||||
|
res->set_acceptance(acc);
|
||||||
|
|
||||||
|
std::vector<unsigned> order;
|
||||||
|
for (unsigned set: todegen.sets())
|
||||||
|
order.push_back(set);
|
||||||
|
|
||||||
|
//auto* names = new std::vector<std::string>;
|
||||||
|
//res->set_named_prop("state-names", names);
|
||||||
|
auto orig_states = new std::vector<unsigned>();
|
||||||
|
auto levels = new std::vector<unsigned>();
|
||||||
|
orig_states->reserve(a->num_states()); // likely more are needed.
|
||||||
|
levels->reserve(a->num_states());
|
||||||
|
res->set_named_prop("original-states", orig_states);
|
||||||
|
res->set_named_prop("degen-levels", levels);
|
||||||
|
|
||||||
|
scc_info si_orig(a, scc_info_options::NONE);
|
||||||
|
|
||||||
|
// degen_states -> new state numbers
|
||||||
|
ds2num_map ds2num;
|
||||||
|
|
||||||
|
queue_t todo;
|
||||||
|
|
||||||
|
auto new_state = [&](degen_state ds)
|
||||||
|
{
|
||||||
|
auto di = ds2num.find(ds);
|
||||||
|
if (di != ds2num.end())
|
||||||
|
return di->second;
|
||||||
|
|
||||||
|
unsigned ns = res->new_state();
|
||||||
|
ds2num[ds] = ns;
|
||||||
|
todo.emplace_back(ds);
|
||||||
|
|
||||||
|
//std::ostringstream os;
|
||||||
|
//os << ds.first << ',' << ds.second;
|
||||||
|
//names->push_back(os.str());
|
||||||
|
assert(ns == orig_states->size());
|
||||||
|
orig_states->emplace_back(ds.first);
|
||||||
|
levels->emplace_back(ds.second);
|
||||||
|
return ns;
|
||||||
|
};
|
||||||
|
degen_state s(a->get_init_state_number(), 0);
|
||||||
|
new_state(s);
|
||||||
|
|
||||||
|
while (!todo.empty())
|
||||||
|
{
|
||||||
|
s = todo.front();
|
||||||
|
todo.pop_front();
|
||||||
|
|
||||||
|
int src = ds2num[s];
|
||||||
|
unsigned slevel = s.second;
|
||||||
|
unsigned orig_src = s.first;
|
||||||
|
unsigned scc_src = si_orig.scc_of(orig_src);
|
||||||
|
for (auto& e: a->out(orig_src))
|
||||||
|
{
|
||||||
|
unsigned nextlvl = slevel;
|
||||||
|
acc_cond::mark_t accepting = {};
|
||||||
|
if (si_orig.scc_of(e.dst) == scc_src)
|
||||||
|
{
|
||||||
|
while (nextlvl < order.size() && e.acc.has(order[nextlvl]))
|
||||||
|
++nextlvl;
|
||||||
|
|
||||||
|
if (nextlvl == order.size())
|
||||||
|
{
|
||||||
|
accepting = degenmark;
|
||||||
|
nextlvl = 0;
|
||||||
|
|
||||||
|
if ((e.acc & todegen) != todegen)
|
||||||
|
while (nextlvl < order.size()
|
||||||
|
&& e.acc.has(order[nextlvl]))
|
||||||
|
++nextlvl;
|
||||||
|
}
|
||||||
|
accepting |= e.acc.strip(tostrip);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextlvl = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
degen_state ds_dst(e.dst, nextlvl);
|
||||||
|
unsigned dst = new_state(ds_dst);
|
||||||
|
res->new_edge(src, dst, e.cond, accepting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res->merge_edges();
|
||||||
|
keep_bottommost_copies(res, si_orig, orig_states);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// -*- coding: utf-8 -*-
|
// -*- coding: utf-8 -*-
|
||||||
// Copyright (C) 2012-2015, 2017, 2018 Laboratoire de
|
// Copyright (C) 2012-2015, 2017-2019 Laboratoire de
|
||||||
// Recherche et Développement de l'Epita.
|
// Recherche et Développement de l'Epita.
|
||||||
//
|
//
|
||||||
// This file is part of Spot, a model checking library.
|
// This file is part of Spot, a model checking library.
|
||||||
|
|
@ -27,7 +27,7 @@ namespace spot
|
||||||
/// \brief Degeneralize a spot::tgba into an equivalent sba with
|
/// \brief Degeneralize a spot::tgba into an equivalent sba with
|
||||||
/// only one acceptance condition.
|
/// only one acceptance condition.
|
||||||
///
|
///
|
||||||
/// This algorithms will build a new explicit automaton that has
|
/// This algorithm will build a new explicit automaton that has
|
||||||
/// at most (N+1) times the number of states of the original automaton.
|
/// at most (N+1) times the number of states of the original automaton.
|
||||||
///
|
///
|
||||||
/// When \a use_z_lvl is set, the level of the degeneralized
|
/// When \a use_z_lvl is set, the level of the degeneralized
|
||||||
|
|
@ -78,4 +78,27 @@ namespace spot
|
||||||
bool ignaccsl = false,
|
bool ignaccsl = false,
|
||||||
bool remove_extra_scc = true);
|
bool remove_extra_scc = true);
|
||||||
/// \@}
|
/// \@}
|
||||||
|
|
||||||
|
/// \ingroup twa_acc_transform
|
||||||
|
/// \brief Partial degeneralization of a TwA
|
||||||
|
///
|
||||||
|
/// Given an automaton whose acceptance contains a conjunction of
|
||||||
|
/// Inf terms, perform a partial degeneralization to replace this
|
||||||
|
/// conjunction by a single Inf term.
|
||||||
|
///
|
||||||
|
/// For instance if the input has acceptance
|
||||||
|
/// (Fin(0)&Inf(1)&Inf(3))|Fin(2)
|
||||||
|
/// calling partial_degeneralize with \a todegen set to `{1,3}`
|
||||||
|
/// will build an equivalent automaton with acceptance
|
||||||
|
/// (Fin(0)&Inf(2))|Fin(1)
|
||||||
|
///
|
||||||
|
/// where Inf(2) tracks the acceptance of the original
|
||||||
|
/// Inf(1)&Inf(3), and Fin(1) tracks the acceptance of the original
|
||||||
|
/// Fin(2).
|
||||||
|
///
|
||||||
|
/// Cases where the sets listed in \a todegen also occur outside
|
||||||
|
/// of the Inf-conjunction are also supported.
|
||||||
|
SPOT_API twa_graph_ptr
|
||||||
|
partial_degeneralize(const const_twa_graph_ptr& a,
|
||||||
|
acc_cond::mark_t todegen);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,7 @@ TESTS_python = \
|
||||||
python/otfcrash.py \
|
python/otfcrash.py \
|
||||||
python/parsetgba.py \
|
python/parsetgba.py \
|
||||||
python/parity.py \
|
python/parity.py \
|
||||||
|
python/pdegen.py \
|
||||||
python/prodexpt.py \
|
python/prodexpt.py \
|
||||||
python/_product_weak.ipynb \
|
python/_product_weak.ipynb \
|
||||||
python/_product_susp.ipynb \
|
python/_product_susp.ipynb \
|
||||||
|
|
|
||||||
98
tests/python/pdegen.py
Normal file
98
tests/python/pdegen.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2019 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/>.
|
||||||
|
|
||||||
|
# Test that the spot.gen package works, in particular, we want
|
||||||
|
# to make sure that the objects created from spot.gen methods
|
||||||
|
# are usable with methods from the spot package.
|
||||||
|
|
||||||
|
|
||||||
|
import spot
|
||||||
|
|
||||||
|
a, b, d = spot.automata("""
|
||||||
|
HOA: v1
|
||||||
|
States: 2
|
||||||
|
Start: 0
|
||||||
|
AP: 1 "p0"
|
||||||
|
Acceptance: 3 Inf(0)&Inf(1)|Fin(2)
|
||||||
|
--BODY--
|
||||||
|
State: 0
|
||||||
|
[0] 1 {1 2}
|
||||||
|
State: 1
|
||||||
|
[0] 0 {0}
|
||||||
|
--END--
|
||||||
|
HOA: v1
|
||||||
|
States: 3
|
||||||
|
Start: 2
|
||||||
|
AP: 1 "p0"
|
||||||
|
Acceptance: 3 Inf(0)&Inf(1)|Fin(2)
|
||||||
|
--BODY--
|
||||||
|
State: 0
|
||||||
|
[0] 1 {1 2}
|
||||||
|
State: 1
|
||||||
|
[0] 0 {0}
|
||||||
|
State: 2
|
||||||
|
[0] 0 {0}
|
||||||
|
[0] 0 {1}
|
||||||
|
--END--
|
||||||
|
HOA: v1
|
||||||
|
States: 1
|
||||||
|
Start: 0
|
||||||
|
AP: 1 "p0"
|
||||||
|
Acceptance: 1 Fin(0)
|
||||||
|
--BODY--
|
||||||
|
State: 0
|
||||||
|
[0] 0 {0}
|
||||||
|
--END--
|
||||||
|
""")
|
||||||
|
|
||||||
|
da = spot.partial_degeneralize(a, [0, 1])
|
||||||
|
assert da.equivalent_to(a)
|
||||||
|
assert da.num_states() == 2
|
||||||
|
|
||||||
|
db = spot.partial_degeneralize(b, [0, 1])
|
||||||
|
assert db.equivalent_to(b)
|
||||||
|
assert db.num_states() == 3
|
||||||
|
|
||||||
|
db.copy_state_names_from(b)
|
||||||
|
dbhoa = db.to_str('hoa')
|
||||||
|
assert dbhoa == """HOA: v1
|
||||||
|
States: 3
|
||||||
|
Start: 0
|
||||||
|
AP: 1 "p0"
|
||||||
|
acc-name: Streett 1
|
||||||
|
Acceptance: 2 Fin(0) | Inf(1)
|
||||||
|
properties: trans-labels explicit-labels state-acc deterministic
|
||||||
|
--BODY--
|
||||||
|
State: 0 "2#0"
|
||||||
|
[0] 2
|
||||||
|
State: 1 "1#0"
|
||||||
|
[0] 2
|
||||||
|
State: 2 "0#1" {0 1}
|
||||||
|
[0] 1
|
||||||
|
--END--"""
|
||||||
|
|
||||||
|
c = spot.automaton("randaut -A'(Fin(0)&Inf(1)&Inf(2))|Fin(2)' 1 |")
|
||||||
|
dc = spot.partial_degeneralize(c, [1, 2])
|
||||||
|
assert dc.equivalent_to(c)
|
||||||
|
assert str(dc.get_acceptance()) == '(Fin(0) & Inf(2)) | Fin(1)'
|
||||||
|
|
||||||
|
dd = spot.partial_degeneralize(d, [])
|
||||||
|
assert dd.equivalent_to(d)
|
||||||
|
assert dd.num_states() == 1
|
||||||
|
assert str(dd.get_acceptance()) == 'Inf(1) & Fin(0)'
|
||||||
Loading…
Add table
Add a link
Reference in a new issue