game: reimplement parity game solving
* spot/misc/game.cc, spot/misc/game.hh: More efficient implementation of Zielonka's algorithm to solve parity games. Now supports SCC decomposition and efficient handling of certain special cases. * doc/org/concepts.org: Document "strategy" and "state-winner" properties. * bin/ltlsynt.cc, tests/python/paritygame.ipynb: Adjust. * tests/core/ltlsynt.test: Add more tests.
This commit is contained in:
parent
f6ac69d0d2
commit
133896d584
6 changed files with 870 additions and 528 deletions
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
#include <cmath>
|
||||
#include <spot/misc/game.hh>
|
||||
#include <spot/misc/bddlt.hh>
|
||||
#include <spot/twaalgos/sccinfo.hh>
|
||||
|
||||
namespace spot
|
||||
{
|
||||
|
|
@ -47,154 +49,728 @@ namespace spot
|
|||
throw std::runtime_error
|
||||
(std::string(fnname) + ": automaton should define \"state-player\"");
|
||||
|
||||
if (owner->size() != arena->num_states())
|
||||
throw
|
||||
(std::string(fnname) + ": \"state-player\" should have "
|
||||
"as many states as the automaton");
|
||||
|
||||
return owner;
|
||||
}
|
||||
|
||||
|
||||
strategy_t attractor(const const_twa_graph_ptr& arena,
|
||||
const std::vector<bool>* owner,
|
||||
const region_t& subgame, region_t& set,
|
||||
unsigned max_parity, int p,
|
||||
bool attr_max)
|
||||
// Internal structs
|
||||
// winning regions for env and player
|
||||
struct winner_t
|
||||
{
|
||||
strategy_t strategy;
|
||||
std::set<unsigned> complement(subgame.begin(), subgame.end());
|
||||
for (unsigned s: set)
|
||||
complement.erase(s);
|
||||
std::vector<bool> has_winner_;
|
||||
std::vector<bool> winner_;
|
||||
|
||||
acc_cond::mark_t max_acc({});
|
||||
for (unsigned i = 0; i <= max_parity; ++i)
|
||||
max_acc.set(i);
|
||||
inline bool operator()(unsigned v, bool p)
|
||||
{
|
||||
// returns true if player p wins v
|
||||
// false otherwise
|
||||
if (!has_winner_[v])
|
||||
return false;
|
||||
|
||||
bool once_more;
|
||||
do
|
||||
{
|
||||
once_more = false;
|
||||
for (auto it = complement.begin(); it != complement.end();)
|
||||
{
|
||||
unsigned s = *it;
|
||||
unsigned i = 0;
|
||||
return winner_[v] == p;
|
||||
}
|
||||
|
||||
bool is_owned = (*owner)[s] == p;
|
||||
bool wins = !is_owned;
|
||||
inline void set(unsigned v, bool p)
|
||||
{
|
||||
has_winner_[v] = true;
|
||||
winner_[v] = p;
|
||||
}
|
||||
|
||||
for (const auto& e: arena->out(s))
|
||||
inline void unset(unsigned v)
|
||||
{
|
||||
has_winner_[v] = false;
|
||||
}
|
||||
|
||||
inline bool winner(unsigned v)
|
||||
{
|
||||
assert(has_winner_.at(v));
|
||||
return winner_[v];
|
||||
}
|
||||
}; // winner_t
|
||||
|
||||
// When using scc decomposition we need to track the
|
||||
// changes made to the graph
|
||||
struct edge_stash_t
|
||||
{
|
||||
edge_stash_t(unsigned num, unsigned dst, acc_cond::mark_t acc) noexcept
|
||||
: e_num(num),
|
||||
e_dst(dst),
|
||||
e_acc(acc)
|
||||
{
|
||||
}
|
||||
const unsigned e_num, e_dst;
|
||||
const acc_cond::mark_t e_acc;
|
||||
}; // edge_stash_t
|
||||
|
||||
// Internal structs used by parity_game
|
||||
// Struct to change recursive calls to stack
|
||||
struct work_t
|
||||
{
|
||||
work_t(unsigned wstep_, unsigned rd_, unsigned min_par_,
|
||||
unsigned max_par_) noexcept
|
||||
: wstep(wstep_),
|
||||
rd(rd_),
|
||||
min_par(min_par_),
|
||||
max_par(max_par_)
|
||||
{
|
||||
}
|
||||
const unsigned wstep, rd, min_par, max_par;
|
||||
}; // work_t
|
||||
|
||||
// Collects information about an scc
|
||||
// Used to detect special cases
|
||||
struct subgame_info_t
|
||||
{
|
||||
typedef std::set<unsigned, std::greater<unsigned>> all_parities_t;
|
||||
|
||||
subgame_info_t() noexcept
|
||||
{
|
||||
}
|
||||
|
||||
subgame_info_t(bool empty, bool one_parity, bool one_player0,
|
||||
bool one_player1, all_parities_t parities) noexcept
|
||||
: is_empty(empty),
|
||||
is_one_parity(one_parity),
|
||||
is_one_player0(one_player0),
|
||||
is_one_player1(one_player1),
|
||||
all_parities(parities)
|
||||
{};
|
||||
bool is_empty; // empty subgame
|
||||
bool is_one_parity; // only one parity appears in the subgame
|
||||
// todo : Not used yet
|
||||
bool is_one_player0; // one player subgame for player0 <-> p==false
|
||||
bool is_one_player1; // one player subgame for player1 <-> p==true
|
||||
all_parities_t all_parities;
|
||||
}; // subgame_info_t
|
||||
|
||||
|
||||
// A class to solve parity games
|
||||
// The current implementation is inspired by
|
||||
// by oink however without multicore and adapted to transition based
|
||||
// acceptance
|
||||
class parity_game
|
||||
{
|
||||
public:
|
||||
|
||||
bool solve(const twa_graph_ptr &arena)
|
||||
{
|
||||
ensure_parity_game(arena, "solve_parity_game()");
|
||||
|
||||
// todo check if reordering states according to scc is worth it
|
||||
set_up(arena);
|
||||
// Start recursive zielonka in a bottom-up fashion on each scc
|
||||
subgame_info_t subgame_info;
|
||||
for (c_scc_idx_ = 0; c_scc_idx_ < info_->scc_count(); ++c_scc_idx_)
|
||||
{
|
||||
// Useless SCCs are winning for player 0.
|
||||
if (!info_->is_useful_scc(c_scc_idx_))
|
||||
{
|
||||
for (unsigned v: c_states())
|
||||
w_.set(v, false);
|
||||
continue;
|
||||
}
|
||||
// Convert transitions leaving edges to self-loops
|
||||
// and check if trivially solvable
|
||||
subgame_info = fix_scc();
|
||||
// If empty, the scc was trivially solved
|
||||
if (!subgame_info.is_empty)
|
||||
{
|
||||
// Check for special cases
|
||||
if (subgame_info.is_one_parity)
|
||||
one_par_subgame_solver(subgame_info, max_abs_par_);
|
||||
else
|
||||
{
|
||||
// "Regular" solver
|
||||
max_abs_par_ = *subgame_info.all_parities.begin();
|
||||
w_stack_.emplace_back(0, 0, 0, max_abs_par_);
|
||||
zielonka();
|
||||
}
|
||||
}
|
||||
}
|
||||
// All done -> restore graph, i.e. undo self-looping
|
||||
restore();
|
||||
|
||||
if (!std::all_of(w_.has_winner_.cbegin(), w_.has_winner_.cend(),
|
||||
[](bool b)
|
||||
{ return b; }))
|
||||
{
|
||||
for (unsigned n = 0; n < w_.has_winner_.size(); ++n)
|
||||
std::cerr << "hw[" << n << "]=" << w_.has_winner_[n] << '\n';
|
||||
}
|
||||
|
||||
assert(std::all_of(w_.has_winner_.cbegin(), w_.has_winner_.cend(),
|
||||
[](bool b)
|
||||
{ return b; }));
|
||||
assert(std::all_of(s_.cbegin(), s_.cend(),
|
||||
[](unsigned e_idx)
|
||||
{ return e_idx > 0; }));
|
||||
|
||||
// Put the solution as named property
|
||||
region_t &w = *arena->get_or_set_named_prop<region_t>("state-winner");
|
||||
strategy_t &s = *arena->get_or_set_named_prop<strategy_t>("strategy");
|
||||
w.swap(w_.winner_);
|
||||
s.resize(s_.size());
|
||||
std::copy(s_.begin(), s_.end(), s.begin());
|
||||
|
||||
clean_up();
|
||||
return w[arena->get_init_state_number()];
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// Returns the vector of states currently considered
|
||||
// i.e. the states of the current scc
|
||||
// c_scc_idx_ is set in the 'main' loop
|
||||
inline const std::vector<unsigned> &c_states()
|
||||
{
|
||||
assert(info_);
|
||||
return info_->states_of(c_scc_idx_);
|
||||
}
|
||||
|
||||
void set_up(const twa_graph_ptr &arena)
|
||||
{
|
||||
owner_ptr_ = arena->get_named_prop<std::vector<bool>>("state-player");
|
||||
arena_ = arena;
|
||||
unsigned n_states = arena_->num_states();
|
||||
// Resize data structures
|
||||
subgame_.clear();
|
||||
subgame_.resize(n_states, unseen_mark);
|
||||
w_.has_winner_.clear();
|
||||
w_.has_winner_.resize(n_states, 0);
|
||||
w_.winner_.clear();
|
||||
w_.winner_.resize(n_states, 0);
|
||||
s_.clear();
|
||||
s_.resize(n_states, -1);
|
||||
// Init
|
||||
rd_ = 0;
|
||||
max_abs_par_ = arena_->get_acceptance().used_sets().max_set() - 1;
|
||||
info_ = std::make_unique<scc_info>(arena_);
|
||||
// Every edge leaving an scc needs to be "fixed"
|
||||
// at some point.
|
||||
// We store: number of edge fixed, original dst, original acc
|
||||
change_stash_.clear();
|
||||
change_stash_.reserve(info_->scc_count() * 2);
|
||||
}
|
||||
|
||||
// Checks if an scc is empty and reports the occurring parities
|
||||
// or special cases
|
||||
inline subgame_info_t
|
||||
inspect_scc(unsigned max_par)
|
||||
{
|
||||
subgame_info_t info;
|
||||
info.is_empty = true;
|
||||
info.is_one_player0 = true;
|
||||
info.is_one_player1 = true;
|
||||
for (unsigned v : c_states())
|
||||
{
|
||||
if (subgame_[v] != unseen_mark)
|
||||
continue;
|
||||
|
||||
bool multi_edge = false;
|
||||
for (const auto &e : arena_->out(v))
|
||||
if (subgame_[e.dst] == unseen_mark)
|
||||
{
|
||||
if ((e.acc & max_acc) && subgame.count(e.dst))
|
||||
info.is_empty = false;
|
||||
unsigned this_par = e.acc.max_set() - 1;
|
||||
if (this_par <= max_par)
|
||||
{
|
||||
if (set.count(e.dst)
|
||||
|| (attr_max && e.acc.max_set() - 1 == max_parity))
|
||||
{
|
||||
if (is_owned)
|
||||
{
|
||||
strategy[s] = i;
|
||||
wins = true;
|
||||
break; // no need to check all edges
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_owned)
|
||||
{
|
||||
wins = false;
|
||||
break; // no need to check all edges
|
||||
}
|
||||
}
|
||||
info.all_parities.insert(this_par);
|
||||
multi_edge = true;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (multi_edge)
|
||||
{
|
||||
// This state has multiple edges, so it is not
|
||||
// a one player subgame for !owner
|
||||
if ((*owner_ptr_)[v])
|
||||
info.is_one_player1 = false;
|
||||
else
|
||||
info.is_one_player0 = false;
|
||||
}
|
||||
} // v
|
||||
assert(info.all_parities.size() || info.is_empty);
|
||||
info.is_one_parity = info.all_parities.size() == 1;
|
||||
// Done
|
||||
return info;
|
||||
}
|
||||
|
||||
if (wins)
|
||||
// Checks if an scc can be trivially solved,
|
||||
// that is, all vertices of the scc belong to the
|
||||
// attractor of a transition leaving the scc
|
||||
inline subgame_info_t
|
||||
fix_scc()
|
||||
{
|
||||
auto scc_acc = info_->acc_sets_of(c_scc_idx_);
|
||||
// We will override all parities of edges leaving the scc
|
||||
bool added[] = {false, false};
|
||||
unsigned par_pair[2];
|
||||
unsigned scc_new_par = std::max(scc_acc.max_set(), 1u);
|
||||
if (scc_new_par&1)
|
||||
{
|
||||
par_pair[1] = scc_new_par;
|
||||
par_pair[0] = scc_new_par+1;
|
||||
}
|
||||
else
|
||||
{
|
||||
par_pair[1] = scc_new_par+1;
|
||||
par_pair[0] = scc_new_par;
|
||||
}
|
||||
acc_cond::mark_t even_mark({par_pair[0]});
|
||||
acc_cond::mark_t odd_mark({par_pair[1]});
|
||||
|
||||
// Only necessary to pass tests
|
||||
max_abs_par_ = std::max(par_pair[0], par_pair[1]);
|
||||
|
||||
for (unsigned v : c_states())
|
||||
{
|
||||
assert(subgame_[v] == unseen_mark);
|
||||
for (auto &e : arena_->out(v))
|
||||
{
|
||||
// The outgoing edges are taken finitely often
|
||||
// -> disregard parity
|
||||
if (subgame_[e.dst] != unseen_mark)
|
||||
{
|
||||
// Edge leaving the scc
|
||||
change_stash_.emplace_back(arena_->edge_number(e),
|
||||
e.dst, e.acc);
|
||||
if (w_.winner(e.dst))
|
||||
{
|
||||
// Winning region of player -> odd
|
||||
e.acc = odd_mark;
|
||||
added[1] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Winning region of env -> even
|
||||
e.acc = even_mark;
|
||||
added[0] = true;
|
||||
}
|
||||
// Replace with self-loop
|
||||
e.dst = e.src;
|
||||
}
|
||||
} // e
|
||||
} // v
|
||||
|
||||
// Compute the attractors of the self-loops/transitions leaving scc
|
||||
// These can be directly added to the winning states
|
||||
// Note: attractors can not intersect therefore the order in which
|
||||
// they are computed does not matter
|
||||
unsigned dummy_rd;
|
||||
|
||||
for (bool p : {false, true})
|
||||
if (added[p])
|
||||
attr(dummy_rd, p, par_pair[p], true, par_pair[p]);
|
||||
|
||||
if (added[0] || added[1])
|
||||
// Fix "negative" strategy
|
||||
for (unsigned v : c_states())
|
||||
if (subgame_[v] != unseen_mark)
|
||||
s_[v] = std::abs(s_[v]);
|
||||
|
||||
return inspect_scc(unseen_mark);
|
||||
} // fix_scc
|
||||
|
||||
inline bool
|
||||
attr(unsigned &rd, bool p, unsigned max_par,
|
||||
bool acc_par, unsigned min_win_par)
|
||||
{
|
||||
// Computes the attractor of the winning set of player p within a
|
||||
// subgame given as rd.
|
||||
// If acc_par is true, max_par transitions are also accepting and
|
||||
// the subgame count will be increased
|
||||
// The attracted vertices are directly added to the set
|
||||
|
||||
// Increase rd meaning we create a new subgame
|
||||
if (acc_par)
|
||||
rd = ++rd_;
|
||||
// todo replace with a variant of topo sort ?
|
||||
// As proposed in Oink! / PGSolver
|
||||
// Needs the transposed graph however
|
||||
|
||||
assert((!acc_par) || (acc_par && (max_par&1) == p));
|
||||
assert(!acc_par || (0 < min_win_par));
|
||||
assert((min_win_par <= max_par) && (max_par <= max_abs_par_));
|
||||
|
||||
bool grown = false;
|
||||
// We could also directly mark states as owned,
|
||||
// instead of adding them to to_add first,
|
||||
// possibly reducing the number of iterations.
|
||||
// However by making the algorithm complete a loop
|
||||
// before adding it is like a backward bfs and (generally) reduces
|
||||
// the size of the strategy
|
||||
static std::vector<unsigned> to_add;
|
||||
|
||||
assert(to_add.empty());
|
||||
|
||||
do
|
||||
{
|
||||
if (!to_add.empty())
|
||||
{
|
||||
grown = true;
|
||||
for (unsigned v : to_add)
|
||||
{
|
||||
// v is winning
|
||||
w_.set(v, p);
|
||||
// Mark if demanded
|
||||
if (acc_par)
|
||||
{
|
||||
assert(subgame_[v] == unseen_mark);
|
||||
subgame_[v] = rd;
|
||||
}
|
||||
}
|
||||
}
|
||||
to_add.clear();
|
||||
|
||||
for (unsigned v : c_states())
|
||||
{
|
||||
if ((subgame_[v] < rd) || (w_(v, p)))
|
||||
// Not in subgame or winning
|
||||
continue;
|
||||
|
||||
bool is_owned = (*owner_ptr_)[v] == p;
|
||||
bool wins = !is_owned;
|
||||
// Loop over out-going
|
||||
|
||||
// Optim: If given the choice,
|
||||
// we seek to go to the "oldest" subgame
|
||||
// That is the subgame with the lowest rd value
|
||||
unsigned min_subgame_idx = -1u;
|
||||
for (const auto &e: arena_->out(v))
|
||||
{
|
||||
unsigned this_par = e.acc.max_set() - 1;
|
||||
if ((subgame_[e.dst] >= rd) && (this_par <= max_par))
|
||||
{
|
||||
// Check if winning
|
||||
if (w_(e.dst, p)
|
||||
|| (acc_par && (min_win_par <= this_par)))
|
||||
{
|
||||
assert(!acc_par || (this_par < min_win_par) ||
|
||||
(acc_par && (min_win_par <= this_par) &&
|
||||
((this_par&1) == p)));
|
||||
if (is_owned)
|
||||
{
|
||||
wins = true;
|
||||
if (acc_par)
|
||||
{
|
||||
s_[v] = arena_->edge_number(e);
|
||||
if (min_win_par <= this_par)
|
||||
// max par edge
|
||||
// change sign -> mark as needs
|
||||
// to be possibly fixed
|
||||
s_[v] = -s_[v];
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
//snapping
|
||||
if (subgame_[e.dst] < min_subgame_idx)
|
||||
{
|
||||
s_[v] = arena_->edge_number(e);
|
||||
min_subgame_idx = subgame_[e.dst];
|
||||
if (!p)
|
||||
// No optim for env
|
||||
break;
|
||||
}
|
||||
}
|
||||
}// owned
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_owned)
|
||||
{
|
||||
wins = false;
|
||||
break;
|
||||
}
|
||||
} // winning
|
||||
} // subgame
|
||||
}// for edges
|
||||
if (wins)
|
||||
to_add.push_back(v);
|
||||
} // for v
|
||||
} while (!to_add.empty());
|
||||
// done
|
||||
|
||||
assert(to_add.empty());
|
||||
return grown;
|
||||
}
|
||||
|
||||
// We need to check if transitions that are accepted due
|
||||
// to their parity remain in the winning region of p
|
||||
inline bool
|
||||
fix_strat_acc(unsigned rd, bool p, unsigned min_win_par, unsigned max_par)
|
||||
{
|
||||
for (unsigned v : c_states())
|
||||
{
|
||||
// Only current attractor and owned
|
||||
// and winning vertices are concerned
|
||||
if ((subgame_[v] != rd) || !w_(v, p)
|
||||
|| ((*owner_ptr_)[v] != p) || (s_[v] > 0))
|
||||
continue;
|
||||
|
||||
// owned winning vertex of attractor
|
||||
// Get the strategy edge
|
||||
s_[v] = -s_[v];
|
||||
const auto &e_s = arena_->edge_storage(s_[v]);
|
||||
// Optimization only for player
|
||||
if (!p && w_(e_s.dst, p))
|
||||
// If current strat is admissible -> nothing to do
|
||||
// for env
|
||||
continue;
|
||||
|
||||
// This is an accepting edge that is no longer admissible
|
||||
// or we seek a more desirable edge (for player)
|
||||
assert(min_win_par <= e_s.acc.max_set() - 1);
|
||||
assert(e_s.acc.max_set() - 1 <= max_par);
|
||||
|
||||
// Strategy heuristic : go to the oldest subgame
|
||||
unsigned min_subgame_idx = -1u;
|
||||
|
||||
s_[v] = -1;
|
||||
for (const auto &e_fix : arena_->out(v))
|
||||
{
|
||||
if (subgame_[e_fix.dst] >= rd)
|
||||
{
|
||||
unsigned this_par = e_fix.acc.max_set() - 1;
|
||||
// This edge must have less than max_par,
|
||||
// otherwise it would have already been attracted
|
||||
assert((this_par <= max_par)
|
||||
|| ((this_par&1) != (max_par&1)));
|
||||
// if it is accepting and leads to the winning region
|
||||
// -> valid fix
|
||||
if ((min_win_par <= this_par)
|
||||
&& (this_par <= max_par)
|
||||
&& w_(e_fix.dst, p)
|
||||
&& (subgame_[e_fix.dst] < min_subgame_idx))
|
||||
{
|
||||
// Max par edge to older subgame found
|
||||
s_[v] = arena_->edge_number(e_fix);
|
||||
min_subgame_idx = subgame_[e_fix.dst];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s_[v] == -1)
|
||||
// NO fix found
|
||||
// This state is NOT won by p due to any accepting edges
|
||||
return true; // true -> grown
|
||||
}
|
||||
// Nothing to fix or all fixed
|
||||
return false; // false -> not grown == all good
|
||||
}
|
||||
|
||||
inline void zielonka()
|
||||
{
|
||||
while (!w_stack_.empty())
|
||||
{
|
||||
auto this_work = w_stack_.back();
|
||||
w_stack_.pop_back();
|
||||
|
||||
switch (this_work.wstep)
|
||||
{
|
||||
case (0):
|
||||
{
|
||||
// FIXME C++17 extract/insert could be useful here
|
||||
set.emplace(s);
|
||||
it = complement.erase(it);
|
||||
once_more = true;
|
||||
assert(this_work.rd == 0);
|
||||
assert(this_work.min_par == 0);
|
||||
|
||||
unsigned rd;
|
||||
assert(this_work.max_par <= max_abs_par_);
|
||||
|
||||
// Check if empty and get parities
|
||||
subgame_info_t subgame_info =
|
||||
inspect_scc(this_work.max_par);
|
||||
|
||||
if (subgame_info.is_empty)
|
||||
// Nothing to do
|
||||
break;
|
||||
if (subgame_info.is_one_parity)
|
||||
{
|
||||
// Can be trivially solved
|
||||
one_par_subgame_solver(subgame_info, this_work.max_par);
|
||||
break;
|
||||
}
|
||||
|
||||
// Compute the winning parity boundaries
|
||||
// -> Priority compression
|
||||
// Optional, improves performance
|
||||
// Highest actually occurring
|
||||
unsigned max_par = *subgame_info.all_parities.begin();
|
||||
unsigned min_win_par = max_par;
|
||||
while ((min_win_par > 2) &&
|
||||
(!subgame_info.all_parities.count(min_win_par-1)))
|
||||
min_win_par -= 2;
|
||||
assert(max_par > 0);
|
||||
assert(!subgame_info.all_parities.empty());
|
||||
assert(min_win_par > 0);
|
||||
|
||||
// Get the player
|
||||
bool p = min_win_par&1;
|
||||
assert((max_par&1) == (min_win_par&1));
|
||||
// Attraction to highest par
|
||||
// This increases rd_ and passes it to rd
|
||||
attr(rd, p, max_par, true, min_win_par);
|
||||
// All those attracted get subgame_[v] <- rd
|
||||
|
||||
// Continuation
|
||||
w_stack_.emplace_back(1, rd, min_win_par, max_par);
|
||||
// Recursion
|
||||
w_stack_.emplace_back(0, 0, 0, min_win_par-1);
|
||||
// Others attracted will have higher counts in subgame
|
||||
break;
|
||||
}
|
||||
else
|
||||
++it;
|
||||
}
|
||||
} while (once_more);
|
||||
return strategy;
|
||||
}
|
||||
case (1):
|
||||
{
|
||||
unsigned rd = this_work.rd;
|
||||
unsigned min_win_par = this_work.min_par;
|
||||
unsigned max_par = this_work.max_par;
|
||||
assert((min_win_par&1) == (max_par&1));
|
||||
bool p = min_win_par&1;
|
||||
// Check if the attractor of w_[!p] is equal to w_[!p]
|
||||
// if so, player wins if there remain accepting transitions
|
||||
// for max_par (see fix_strat_acc)
|
||||
// This does not increase but reuse rd
|
||||
bool grown = attr(rd, !p, max_par, false, min_win_par);
|
||||
// todo investigate: A is an attractor, so the only way that
|
||||
// attr(w[!p]) != w[!p] is if the max par "exit" edges lead
|
||||
// to a trap for player/ exit the winning region of the
|
||||
// player so we can do a fast check instead of the
|
||||
// generic attr computation which only needs to be done
|
||||
// if the fast check is positive
|
||||
|
||||
void solve_rec(const const_twa_graph_ptr& arena,
|
||||
const std::vector<bool>* owner,
|
||||
region_t& subgame, unsigned max_parity,
|
||||
region_t (&w)[2], strategy_t (&s)[2])
|
||||
{
|
||||
assert(w[0].empty());
|
||||
assert(w[1].empty());
|
||||
assert(s[0].empty());
|
||||
assert(s[1].empty());
|
||||
// The algorithm works recursively on subgames. To avoid useless copies of
|
||||
// the game at each call, subgame and max_parity are used to filter states
|
||||
// and transitions.
|
||||
if (subgame.empty())
|
||||
return;
|
||||
int p = max_parity % 2;
|
||||
// Check if strategy needs to be fixed / is fixable
|
||||
if (!grown)
|
||||
// this only concerns parity accepting edges
|
||||
grown = fix_strat_acc(rd, p, min_win_par, max_par);
|
||||
// If !grown we are done, and the partitions are valid
|
||||
|
||||
// Recursion on max_parity.
|
||||
region_t u;
|
||||
auto strat_u = attractor(arena, owner, subgame, u, max_parity, p, true);
|
||||
if (grown)
|
||||
{
|
||||
// Reset current game without !p attractor
|
||||
for (unsigned v : c_states())
|
||||
if (!w_(v, !p) && (subgame_[v] >= rd))
|
||||
{
|
||||
// delete ownership
|
||||
w_.unset(v);
|
||||
// Mark as unseen
|
||||
subgame_[v] = unseen_mark;
|
||||
// Unset strat for testing
|
||||
s_[v] = -1;
|
||||
}
|
||||
w_stack_.emplace_back(0, 0, 0, max_par);
|
||||
// No need to do anything else
|
||||
// the attractor of !p of this level is not changed
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error("No valid workstep");
|
||||
} // switch
|
||||
} // while
|
||||
} // zielonka
|
||||
|
||||
if (max_parity == 0)
|
||||
{
|
||||
s[p] = std::move(strat_u);
|
||||
w[p] = std::move(u);
|
||||
// FIXME what about w[!p]?
|
||||
return;
|
||||
}
|
||||
// Undo change to the graph made along the way
|
||||
inline void restore()
|
||||
{
|
||||
// "Unfix" the edges leaving the sccs
|
||||
// This is called once the game has been solved
|
||||
for (auto &e_stash : change_stash_)
|
||||
{
|
||||
auto &e = arena_->edge_storage(e_stash.e_num);
|
||||
e.dst = e_stash.e_dst;
|
||||
e.acc = e_stash.e_acc;
|
||||
}
|
||||
// Done
|
||||
}
|
||||
|
||||
for (unsigned s: u)
|
||||
subgame.erase(s);
|
||||
region_t w0[2]; // Player's winning region in the first recursive call.
|
||||
strategy_t s0[2]; // Player's winning strategy in the first
|
||||
// recursive call.
|
||||
solve_rec(arena, owner, subgame, max_parity - 1, w0, s0);
|
||||
if (w0[0].size() + w0[1].size() != subgame.size())
|
||||
throw std::runtime_error("size mismatch");
|
||||
//if (w0[p].size() != subgame.size())
|
||||
// for (unsigned s: subgame)
|
||||
// if (w0[p].find(s) == w0[p].end())
|
||||
// w0[!p].insert(s);
|
||||
subgame.insert(u.begin(), u.end());
|
||||
// Empty all internal variables
|
||||
inline void clean_up()
|
||||
{
|
||||
info_ = nullptr;
|
||||
subgame_.clear();
|
||||
w_.has_winner_.clear();
|
||||
w_.winner_.clear();
|
||||
s_.clear();
|
||||
rd_ = 0;
|
||||
max_abs_par_ = 0;
|
||||
change_stash_.clear();
|
||||
}
|
||||
|
||||
if (w0[p].size() + u.size() == subgame.size())
|
||||
{
|
||||
s[p] = std::move(strat_u);
|
||||
s[p].insert(s0[p].begin(), s0[p].end());
|
||||
w[p].insert(subgame.begin(), subgame.end());
|
||||
return;
|
||||
}
|
||||
// Dedicated solver for special cases
|
||||
inline void one_par_subgame_solver(const subgame_info_t &info,
|
||||
unsigned max_par)
|
||||
{
|
||||
assert(info.all_parities.size() == 1);
|
||||
// The entire subgame is won by the player of the only parity
|
||||
// Any edge will do
|
||||
// todo optim for smaller circuit
|
||||
// This subgame gets its own counter
|
||||
++rd_;
|
||||
unsigned rd = rd_;
|
||||
unsigned one_par = *info.all_parities.begin();
|
||||
bool winner = one_par & 1;
|
||||
assert(one_par <= max_par);
|
||||
|
||||
// Recursion on game size.
|
||||
auto strat_wnp = attractor(arena, owner,
|
||||
subgame, w0[!p], max_parity, !p, false);
|
||||
for (unsigned v : c_states())
|
||||
{
|
||||
if (subgame_[v] != unseen_mark)
|
||||
continue;
|
||||
// State of the subgame
|
||||
subgame_[v] = rd;
|
||||
w_.set(v, winner);
|
||||
// Get the strategy
|
||||
assert(s_[v] == -1);
|
||||
for (const auto &e : arena_->out(v))
|
||||
{
|
||||
unsigned this_par = e.acc.max_set() - 1;
|
||||
if ((subgame_[e.dst] >= rd) && (this_par <= max_par))
|
||||
{
|
||||
assert(this_par == one_par);
|
||||
// Ok for strat
|
||||
s_[v] = arena_->edge_number(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert((0 < s_[v]) && (s_[v] < unseen_mark));
|
||||
}
|
||||
// Done
|
||||
}
|
||||
|
||||
for (unsigned s: w0[!p])
|
||||
subgame.erase(s);
|
||||
const unsigned unseen_mark = std::numeric_limits<unsigned>::max();
|
||||
|
||||
region_t w1[2]; // Odd's winning region in the second recursive call.
|
||||
strategy_t s1[2]; // Odd's winning strategy in the second recursive call.
|
||||
solve_rec(arena, owner, subgame, max_parity, w1, s1);
|
||||
if (w1[0].size() + w1[1].size() != subgame.size())
|
||||
throw std::runtime_error("size mismatch");
|
||||
twa_graph_ptr arena_;
|
||||
const std::vector<bool> *owner_ptr_;
|
||||
unsigned rd_;
|
||||
winner_t w_;
|
||||
// Subgame array similar to the one from oink!
|
||||
std::vector<unsigned> subgame_;
|
||||
// strategies for env and player; For synthesis only player is needed
|
||||
// We need a signed value here in order to "fix" the strategy
|
||||
// during construction
|
||||
std::vector<long long> s_;
|
||||
|
||||
w[p] = std::move(w1[p]);
|
||||
s[p] = std::move(s1[p]);
|
||||
|
||||
w[!p] = std::move(w1[!p]);
|
||||
w[!p].insert(w0[!p].begin(), w0[!p].end());
|
||||
s[!p] = std::move(strat_wnp);
|
||||
s[!p].insert(s0[!p].begin(), s0[!p].end());
|
||||
s[!p].insert(s1[!p].begin(), s1[!p].end());
|
||||
|
||||
subgame.insert(w0[!p].begin(), w0[!p].end());
|
||||
}
|
||||
// Informations about sccs andthe current scc
|
||||
std::unique_ptr<scc_info> info_;
|
||||
unsigned max_abs_par_; // Max parity occurring in the current scc
|
||||
// Info on the current scc
|
||||
unsigned c_scc_idx_;
|
||||
// Fixes made to the sccs that have to be undone
|
||||
// before returning
|
||||
std::vector<edge_stash_t> change_stash_;
|
||||
// Change recursive calls to stack
|
||||
std::vector<work_t> w_stack_;
|
||||
};
|
||||
|
||||
} // anonymous
|
||||
|
||||
bool solve_parity_game(const twa_graph_ptr& arena)
|
||||
{
|
||||
parity_game pg;
|
||||
return pg.solve(arena);
|
||||
}
|
||||
|
||||
void pg_print(std::ostream& os, const const_twa_graph_ptr& arena)
|
||||
{
|
||||
auto owner = ensure_parity_game(arena, "pg_print");
|
||||
|
|
@ -230,35 +806,13 @@ namespace spot
|
|||
}
|
||||
}
|
||||
|
||||
solved_game parity_game_solve(const const_twa_graph_ptr& arena)
|
||||
{
|
||||
solved_game result;
|
||||
result.arena = arena;
|
||||
|
||||
const std::vector<bool>* owner =
|
||||
ensure_parity_game(arena, "parity_game_solve");
|
||||
|
||||
region_t states_;
|
||||
unsigned ns = arena->num_states();
|
||||
for (unsigned i = 0; i < ns; ++i)
|
||||
states_.insert(i);
|
||||
|
||||
acc_cond::mark_t m{};
|
||||
for (const auto& e: arena->edges())
|
||||
m |= e.acc;
|
||||
|
||||
solve_rec(arena, owner, states_, m.max_set(),
|
||||
result.winning_region, result.winning_strategy);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void propagate_players(spot::twa_graph_ptr& arena,
|
||||
bool first_player, bool complete0)
|
||||
{
|
||||
auto um = arena->acc().unsat_mark();
|
||||
if (!um.first)
|
||||
throw std::runtime_error("game winning condition is a tautology");
|
||||
throw std::runtime_error
|
||||
("propagate_players(): game winning condition is a tautology");
|
||||
|
||||
unsigned sink_env = 0;
|
||||
unsigned sink_con = 0;
|
||||
|
|
@ -311,45 +865,39 @@ namespace spot
|
|||
}
|
||||
|
||||
twa_graph_ptr
|
||||
highlight_strategy(twa_graph_ptr& aut, const strategy_t& s,
|
||||
unsigned color)
|
||||
highlight_strategy(twa_graph_ptr& aut,
|
||||
int player0_color,
|
||||
int player1_color)
|
||||
{
|
||||
unsigned ns = aut->num_states();
|
||||
auto* highlight = aut->get_or_set_named_prop<std::map<unsigned, unsigned>>
|
||||
("highlight-edges");
|
||||
auto owner = ensure_parity_game(aut, "highlight_strategy()");
|
||||
region_t* w = aut->get_named_prop<region_t>("state-winner");
|
||||
strategy_t* s = aut->get_named_prop<strategy_t>("strategy");
|
||||
if (!w || !s)
|
||||
throw std::runtime_error("highlight_strategy(): "
|
||||
"strategy not available, solve the game first");
|
||||
|
||||
for (auto [src, n]: s)
|
||||
unsigned ns = aut->num_states();
|
||||
auto* hl_edges = aut->get_or_set_named_prop<std::map<unsigned, unsigned>>
|
||||
("highlight-edges");
|
||||
auto* hl_states = aut->get_or_set_named_prop<std::map<unsigned, unsigned>>
|
||||
("highlight-states");
|
||||
|
||||
if (unsigned sz = std::min(w->size(), s->size()); sz < ns)
|
||||
ns = sz;
|
||||
|
||||
for (unsigned n = 0; n < ns; ++n)
|
||||
{
|
||||
if (src >= ns)
|
||||
throw std::runtime_error
|
||||
("highlight_strategy(): strategy refers to unexisting states");
|
||||
unsigned int i = 0;
|
||||
for (auto& t: aut->out(src))
|
||||
if (i++ == n)
|
||||
{
|
||||
(*highlight)[aut->edge_number(t)] = color;
|
||||
break;
|
||||
}
|
||||
int color = (*w)[n] ? player1_color : player0_color;
|
||||
if (color == -1)
|
||||
continue;
|
||||
(*hl_states)[n] = color;
|
||||
if ((*w)[n] == (*owner)[n])
|
||||
(*hl_edges)[(*s)[n]] = color;
|
||||
}
|
||||
|
||||
return aut;
|
||||
}
|
||||
|
||||
twa_graph_ptr
|
||||
solved_game::highlight_strategy(unsigned player, unsigned color)
|
||||
{
|
||||
auto aut = std::const_pointer_cast<twa_graph>(arena);
|
||||
|
||||
auto* highlight = aut->get_or_set_named_prop<std::map<unsigned, unsigned>>
|
||||
("highlight-states");
|
||||
unsigned ns = aut->num_states();
|
||||
for (unsigned i = 0; i < ns; ++i)
|
||||
if (player_winning_at(player, i))
|
||||
(*highlight)[i] = color;
|
||||
|
||||
return spot::highlight_strategy(aut, winning_strategy[!!player], color);
|
||||
}
|
||||
|
||||
void set_state_players(twa_graph_ptr arena, std::vector<bool> owners)
|
||||
{
|
||||
std::vector<bool>* owners_ptr = new std::vector<bool>(owners);
|
||||
|
|
|
|||
|
|
@ -47,27 +47,12 @@ namespace spot
|
|||
bool complete0 = true);
|
||||
|
||||
|
||||
typedef std::unordered_set<unsigned> region_t;
|
||||
typedef std::unordered_map<unsigned, unsigned> strategy_t;
|
||||
// false -> env, true -> player
|
||||
typedef std::vector<bool> region_t;
|
||||
// state idx -> global edge number
|
||||
typedef std::vector<unsigned> strategy_t;
|
||||
|
||||
|
||||
struct SPOT_API solved_game
|
||||
{
|
||||
const_twa_graph_ptr arena;
|
||||
|
||||
region_t winning_region[2];
|
||||
strategy_t winning_strategy[2];
|
||||
|
||||
/// \brief Highlight the edges of a strategy on the automaton.
|
||||
twa_graph_ptr highlight_strategy(unsigned player, unsigned color);
|
||||
|
||||
bool player_winning_at(unsigned player, unsigned state)
|
||||
{
|
||||
auto& w = winning_region[player];
|
||||
return w.find(state) != w.end();
|
||||
}
|
||||
};
|
||||
|
||||
/// \brief solve a parity-game
|
||||
///
|
||||
/// The arena is a deterministic max odd parity automaton with a
|
||||
|
|
@ -76,8 +61,11 @@ namespace spot
|
|||
/// This computes the winning strategy and winning region of this
|
||||
/// game for player 1 using Zielonka's recursive algorithm.
|
||||
/// \cite zielonka.98.tcs
|
||||
///
|
||||
/// Return the player winning in the initial state, and set
|
||||
/// the state-winner and strategy named properties.
|
||||
SPOT_API
|
||||
solved_game parity_game_solve(const const_twa_graph_ptr& arena);
|
||||
bool solve_parity_game(const twa_graph_ptr& arena);
|
||||
|
||||
/// \brief Print a max odd parity game using PG-solver syntax
|
||||
SPOT_API
|
||||
|
|
@ -85,10 +73,12 @@ namespace spot
|
|||
|
||||
|
||||
/// \brief Highlight the edges of a strategy on an automaton.
|
||||
///
|
||||
/// Pass a negative color to not display the corresponding strategy.
|
||||
SPOT_API
|
||||
twa_graph_ptr highlight_strategy(twa_graph_ptr& arena,
|
||||
const strategy_t& s,
|
||||
unsigned color);
|
||||
int player0_color = 5,
|
||||
int player1_color = 4);
|
||||
|
||||
/// \brief Set the owner for all the states.
|
||||
SPOT_API
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue