Adding selective edge sorting and state merging
The merging is (possibly) more expensive but also merges more states when applied to all states. * spot/graph/graph.hh: edge sorting * spot/twa/twagraph.cc, spot/twa/twagraph.hh: selective state merging * tests/core/twagraph.cc: Adjusting tests
This commit is contained in:
parent
2c267dd894
commit
786599ed20
4 changed files with 280 additions and 1 deletions
|
|
@ -1226,6 +1226,51 @@ namespace spot
|
||||||
std::stable_sort(edges_.begin() + 1, edges_.end(), p);
|
std::stable_sort(edges_.begin() + 1, edges_.end(), p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// \brief Sort edges of the given states
|
||||||
|
///
|
||||||
|
/// \tparam Predicate : Comparison type
|
||||||
|
/// \param p : Comparison callable
|
||||||
|
/// \param to_sort_ptr : which states to sort. If null, all will be sorted
|
||||||
|
/// \note No need to call chain_edges_, they are in a coherent state.
|
||||||
|
/// todo: If pred does not involve bdd action other than id -> parallelize
|
||||||
|
template<bool Stable = false, class Predicate = std::less<edge_storage_t>>
|
||||||
|
void sort_edges_of_(Predicate p = Predicate(),
|
||||||
|
const std::vector<bool>* to_sort_ptr = nullptr)
|
||||||
|
{
|
||||||
|
SPOT_ASSERT((to_sort_ptr == nullptr)
|
||||||
|
|| (to_sort_ptr->size() == num_states()));
|
||||||
|
//std::cerr << "\nbefore\n";
|
||||||
|
//dump_storage(std::cerr);
|
||||||
|
auto pi = [&](unsigned t1, unsigned t2)
|
||||||
|
{return p(edges_[t1], edges_[t2]); };
|
||||||
|
std::vector<unsigned> sort_idx_;
|
||||||
|
for (unsigned i = 0; i < num_states(); ++i)
|
||||||
|
{
|
||||||
|
if (to_sort_ptr && !(*to_sort_ptr)[i])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sort_idx_.clear();
|
||||||
|
unsigned t = states_[i].succ;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
sort_idx_.push_back(t);
|
||||||
|
t = edges_[t].next_succ;
|
||||||
|
} while (t != 0);
|
||||||
|
if constexpr (Stable)
|
||||||
|
std::stable_sort(sort_idx_.begin(), sort_idx_.end(), pi);
|
||||||
|
else
|
||||||
|
std::sort(sort_idx_.begin(), sort_idx_.end(), pi);
|
||||||
|
// Update the graph
|
||||||
|
states_[i].succ = sort_idx_.front();
|
||||||
|
states_[i].succ_tail = sort_idx_.back();
|
||||||
|
const unsigned n_outs_n1 = sort_idx_.size() - 1;
|
||||||
|
for (unsigned k = 0; k < n_outs_n1; ++k)
|
||||||
|
edges_[sort_idx_[k]].next_succ = sort_idx_[k+1];
|
||||||
|
edges_[sort_idx_.back()].next_succ = 0; // terminal
|
||||||
|
}
|
||||||
|
// Done
|
||||||
|
}
|
||||||
|
|
||||||
/// \brief Reconstruct the chain of outgoing edges
|
/// \brief Reconstruct the chain of outgoing edges
|
||||||
///
|
///
|
||||||
/// Should be called only when it is known that all edges
|
/// Should be called only when it is known that all edges
|
||||||
|
|
|
||||||
|
|
@ -385,6 +385,153 @@ namespace spot
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned twa_graph::merge_states_of(bool stable,
|
||||||
|
const std::vector<bool>* to_merge_ptr)
|
||||||
|
{
|
||||||
|
if (!is_existential())
|
||||||
|
throw std::runtime_error(
|
||||||
|
"twa_graph::merge_states() does not work on alternating automata");
|
||||||
|
|
||||||
|
typedef graph_t::edge_storage_t tr_t;
|
||||||
|
if (stable)
|
||||||
|
g_.sort_edges_of_<true>([](const tr_t& lhs, const tr_t& rhs)
|
||||||
|
{
|
||||||
|
if (lhs.acc < rhs.acc)
|
||||||
|
return true;
|
||||||
|
if (lhs.acc > rhs.acc)
|
||||||
|
return false;
|
||||||
|
if (bdd_less_than_stable lt; lt(lhs.cond, rhs.cond))
|
||||||
|
return true;
|
||||||
|
if (rhs.cond != lhs.cond)
|
||||||
|
return false;
|
||||||
|
// The destination must be sorted last
|
||||||
|
// for our self-loop optimization to work.
|
||||||
|
return lhs.dst < rhs.dst;
|
||||||
|
});
|
||||||
|
else
|
||||||
|
g_.sort_edges_of_<false>([](const tr_t& lhs, const tr_t& rhs)
|
||||||
|
{
|
||||||
|
if (lhs.acc < rhs.acc)
|
||||||
|
return true;
|
||||||
|
if (lhs.acc > rhs.acc)
|
||||||
|
return false;
|
||||||
|
if (bdd_less_than lt; lt(lhs.cond, rhs.cond))
|
||||||
|
return true;
|
||||||
|
if (rhs.cond != lhs.cond)
|
||||||
|
return false;
|
||||||
|
// The destination must be sorted last
|
||||||
|
// for our self-loop optimization to work.
|
||||||
|
return lhs.dst < rhs.dst;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Associates a hash value to a vector of classes
|
||||||
|
std::unordered_map<size_t, std::vector<std::set<unsigned>>> equiv_class_;
|
||||||
|
|
||||||
|
auto hash_state_ = [&](unsigned s)->size_t
|
||||||
|
{
|
||||||
|
// Hash the edges
|
||||||
|
// bottle_neck?
|
||||||
|
size_t h = fnv<size_t>::init;
|
||||||
|
for (const edge_storage_t& e : out(s))
|
||||||
|
{
|
||||||
|
h ^= knuth32_hash(e.dst);
|
||||||
|
h ^= knuth32_hash(e.cond.id());
|
||||||
|
h ^= e.acc.hash();
|
||||||
|
h = wang32_hash(h);
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned nb_states = num_states();
|
||||||
|
|
||||||
|
std::vector<unsigned> comp_classes_; // Classes to be merged
|
||||||
|
for (unsigned i = 0; i != nb_states; ++i)
|
||||||
|
{
|
||||||
|
if (to_merge_ptr && !(*to_merge_ptr)[nb_states])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
size_t hi = hash_state_(i);
|
||||||
|
auto equal_to_i_ = [&, outi = out(i)](unsigned j)
|
||||||
|
{
|
||||||
|
auto outj = out(j);
|
||||||
|
return std::equal(outi.begin(), outi.end(),
|
||||||
|
outj.begin(), outj.end(),
|
||||||
|
[](const edge_storage_t& a,
|
||||||
|
const edge_storage_t& b)
|
||||||
|
{ return ((a.dst == b.dst
|
||||||
|
|| (a.dst == a.src && b.dst == b.src))
|
||||||
|
&& a.data() == b.data()); });
|
||||||
|
};
|
||||||
|
comp_classes_.clear();
|
||||||
|
// get all compatible classes
|
||||||
|
// Candidate classes share a hash
|
||||||
|
// A state is compatible to a class if it is compatble
|
||||||
|
// to any of its states
|
||||||
|
auto& cand_classes = equiv_class_[hi];
|
||||||
|
unsigned n_c_classes = cand_classes.size();
|
||||||
|
// Built it in "reverse order" the idx of
|
||||||
|
// classes to be merged
|
||||||
|
for (unsigned nc = n_c_classes - 1; nc < n_c_classes; --nc)
|
||||||
|
if (std::any_of(cand_classes[nc].begin(),
|
||||||
|
cand_classes[nc].end(),
|
||||||
|
[&](unsigned j)
|
||||||
|
{return equal_to_i_(j); }))
|
||||||
|
comp_classes_.push_back(nc);
|
||||||
|
// Possible results:
|
||||||
|
// 1) comp_classes_ is empty -> i gets its own class
|
||||||
|
// 2) fuse together all comp_classes and add i
|
||||||
|
if (comp_classes_.empty())
|
||||||
|
cand_classes.emplace_back(std::set<unsigned>({i}));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Lowest idx
|
||||||
|
auto& base_class = cand_classes[comp_classes_.back()];
|
||||||
|
comp_classes_.pop_back(); // Keep this one
|
||||||
|
for (unsigned compi : comp_classes_)
|
||||||
|
{
|
||||||
|
// fuse with base and delete
|
||||||
|
base_class.insert(cand_classes[compi].begin(),
|
||||||
|
cand_classes[compi].end());
|
||||||
|
std::swap(cand_classes[compi], cand_classes.back());
|
||||||
|
cand_classes.pop_back();
|
||||||
|
}
|
||||||
|
// finally add the current state that caused all the merging
|
||||||
|
base_class.emplace_hint(base_class.end(), i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now we have equivalence classes
|
||||||
|
// and a state can only be in exactly
|
||||||
|
// (Otherwise the classes would have fused)
|
||||||
|
// For each equiv class we take the first state as representative
|
||||||
|
// and redirect all incoming edges to this one
|
||||||
|
std::vector<unsigned> remap(nb_states, -1U);
|
||||||
|
for (const auto& [_, class_v] : equiv_class_)
|
||||||
|
for (const auto& aclass : class_v)
|
||||||
|
{
|
||||||
|
unsigned rep = *aclass.begin();
|
||||||
|
for (auto it = ++aclass.begin(); it != aclass.end(); ++it)
|
||||||
|
remap[*it] = rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& e: edges())
|
||||||
|
if (remap[e.dst] != -1U)
|
||||||
|
e.dst = remap[e.dst];
|
||||||
|
|
||||||
|
if (remap[get_init_state_number()] != -1U)
|
||||||
|
set_init_state(remap[get_init_state_number()]);
|
||||||
|
|
||||||
|
unsigned st = 0;
|
||||||
|
for (auto& s: remap)
|
||||||
|
if (s == -1U)
|
||||||
|
s = st++;
|
||||||
|
else
|
||||||
|
s = -1U;
|
||||||
|
|
||||||
|
defrag_states(remap, st);
|
||||||
|
return remap.size() - st;
|
||||||
|
}
|
||||||
|
|
||||||
void twa_graph::purge_unreachable_states(shift_action* f, void* action_data)
|
void twa_graph::purge_unreachable_states(shift_action* f, void* action_data)
|
||||||
{
|
{
|
||||||
unsigned num_states = g_.num_states();
|
unsigned num_states = g_.num_states();
|
||||||
|
|
|
||||||
|
|
@ -593,6 +593,19 @@ namespace spot
|
||||||
/// \return the number of states that have been merged and removed.
|
/// \return the number of states that have been merged and removed.
|
||||||
unsigned merge_states();
|
unsigned merge_states();
|
||||||
|
|
||||||
|
/// \brief Like merge states, but one can chose which states are
|
||||||
|
/// candidates for merging.
|
||||||
|
///
|
||||||
|
/// \param stable Determines whether or not a stable sorting is used for
|
||||||
|
/// the edges
|
||||||
|
/// \param to_merge_ptr Determines which states are candidates.
|
||||||
|
/// If null, all states are considered
|
||||||
|
/// The actual implementation differd from merge_states().
|
||||||
|
/// It is more costly, but is more precise, in the sense that
|
||||||
|
/// more states are merged.
|
||||||
|
unsigned merge_states_of(bool stable = true,
|
||||||
|
const std::vector<bool>* to_merge_ptr = nullptr);
|
||||||
|
|
||||||
/// \brief Remove all dead states
|
/// \brief Remove all dead states
|
||||||
///
|
///
|
||||||
/// Dead states are all the states that cannot be part of
|
/// Dead states are all the states that cannot be part of
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,9 @@ static void f1()
|
||||||
// Test purge with named and highlighted states.
|
// Test purge with named and highlighted states.
|
||||||
static void f2()
|
static void f2()
|
||||||
{
|
{
|
||||||
auto d = spot::make_bdd_dict();
|
auto
|
||||||
|
|
||||||
|
d = spot::make_bdd_dict();
|
||||||
auto tg = make_twa_graph(d);
|
auto tg = make_twa_graph(d);
|
||||||
|
|
||||||
auto s1 = tg->new_state();
|
auto s1 = tg->new_state();
|
||||||
|
|
@ -186,6 +188,76 @@ static void f5()
|
||||||
spot::print_hoa(std::cout, tg) << '\n';
|
spot::print_hoa(std::cout, tg) << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test merge_states_of()
|
||||||
|
static void f6()
|
||||||
|
{
|
||||||
|
auto d = spot::make_bdd_dict();
|
||||||
|
auto tg = make_twa_graph(d);
|
||||||
|
|
||||||
|
auto s1 = tg->new_state();
|
||||||
|
auto s2 = tg->new_state();
|
||||||
|
auto s3 = tg->new_state();
|
||||||
|
auto s4 = tg->new_state();
|
||||||
|
auto s5 = tg->new_state();
|
||||||
|
|
||||||
|
tg->set_init_state(s5);
|
||||||
|
tg->new_edge(s1, s2, bddtrue);
|
||||||
|
tg->new_edge(s2, s2, bddtrue);
|
||||||
|
tg->new_edge(s3, s2, bddtrue);
|
||||||
|
tg->new_edge(s4, s4, bddtrue);
|
||||||
|
tg->new_edge(s5, s1, bddtrue);
|
||||||
|
tg->new_edge(s5, s2, bddtrue);
|
||||||
|
tg->new_edge(s5, s3, bddtrue);
|
||||||
|
tg->new_edge(s5, s4, bddtrue);
|
||||||
|
|
||||||
|
unsigned out = tg->merge_states_of();
|
||||||
|
assert(out == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare merge_states() and merge_states_of()
|
||||||
|
// when faced with a more involved problem
|
||||||
|
static void f7()
|
||||||
|
{
|
||||||
|
// The current mege_states implementation of "next"
|
||||||
|
// needs two successive calls to obtain an automaton with only 3 states
|
||||||
|
// This is especially annoying as this depends on the numbering.
|
||||||
|
// By renumbering 2->1 3->2 1->3 the current version only needs one call too
|
||||||
|
auto get_aut = []()
|
||||||
|
{
|
||||||
|
auto d = spot::make_bdd_dict();
|
||||||
|
auto aut = make_twa_graph(d);
|
||||||
|
|
||||||
|
aut->new_states(5);
|
||||||
|
|
||||||
|
aut->new_edge(0, 1, bddtrue);
|
||||||
|
aut->new_edge(0, 2, bddtrue);
|
||||||
|
|
||||||
|
aut->new_edge(1, 1, bddtrue);
|
||||||
|
aut->new_edge(1, 4, bddtrue);
|
||||||
|
|
||||||
|
aut->new_edge(2, 3, bddtrue);
|
||||||
|
aut->new_edge(2, 4, bddtrue);
|
||||||
|
|
||||||
|
aut->new_edge(3, 3, bddtrue);
|
||||||
|
aut->new_edge(3, 4, bddtrue);
|
||||||
|
|
||||||
|
return aut;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto aut = get_aut();
|
||||||
|
// Basic merge_states needs 2 iterations
|
||||||
|
// Merging only one step each
|
||||||
|
assert(aut->merge_states() == 1u);
|
||||||
|
assert(aut->merge_states() == 1u);
|
||||||
|
assert(aut->num_states() == 3u);
|
||||||
|
|
||||||
|
// merge_states_of can do it in one iteration
|
||||||
|
// when used on all states
|
||||||
|
aut = get_aut();
|
||||||
|
assert(aut->merge_states_of() == 2u);
|
||||||
|
assert(aut->num_states() == 3u);
|
||||||
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
f1();
|
f1();
|
||||||
|
|
@ -193,4 +265,6 @@ int main()
|
||||||
f3();
|
f3();
|
||||||
f4();
|
f4();
|
||||||
f5();
|
f5();
|
||||||
|
f6();
|
||||||
|
f7();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue