Commit 133896d5 authored by Philipp Schlehuber's avatar Philipp Schlehuber Committed by Alexandre Duret-Lutz
Browse files

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.
parent f6ac69d0
Pipeline #21699 passed with stages
in 311 minutes and 2 seconds
......@@ -212,51 +212,101 @@ namespace
return dpa;
}
// Construct a smaller automaton, filtering out states that are not
// accessible. Also merge back pairs of p --(i)--> q --(o)--> r
// transitions to p --(i&o)--> r.
static spot::twa_graph_ptr
strat_to_aut(const spot::const_twa_graph_ptr& pg,
const spot::strategy_t& strat,
bdd all_outputs)
{
auto aut = spot::make_twa_graph(pg->get_dict());
aut->copy_ap_of(pg);
unsigned pg_init = pg->get_init_state_number();
std::vector<unsigned> todo{pg_init};
std::vector<int> pg2aut(pg->num_states(), -1);
spot::twa_graph_ptr
apply_strategy(const spot::twa_graph_ptr& arena,
bdd all_outputs,
bool unsplit, bool keep_acc, bool leave_choice)
{
spot::region_t* w_ptr =
arena->get_named_prop<spot::region_t>("state-winner");
spot::strategy_t* s_ptr =
arena->get_named_prop<spot::strategy_t>("strategy");
std::vector<bool>* sp_ptr =
arena->get_named_prop<std::vector<bool>>("state-player");
if (!w_ptr || !s_ptr || !sp_ptr)
throw std::runtime_error("Arena missing state-winner, strategy "
"or state-player");
if (!(w_ptr->at(arena->get_init_state_number())))
throw std::runtime_error("Player does not win initial state, strategy "
"is not applicable");
spot::region_t& w = *w_ptr;
spot::strategy_t& s = *s_ptr;
auto aut = spot::make_twa_graph(arena->get_dict());
aut->copy_ap_of(arena);
if (keep_acc)
aut->copy_acceptance_of(arena);
const unsigned unseen_mark = std::numeric_limits<unsigned>::max();
std::vector<unsigned> todo{arena->get_init_state_number()};
std::vector<unsigned> pg2aut(arena->num_states(), unseen_mark);
aut->set_init_state(aut->new_state());
pg2aut[pg_init] = aut->get_init_state_number();
pg2aut[arena->get_init_state_number()] = aut->get_init_state_number();
bdd out;
while (!todo.empty())
{
unsigned s = todo.back();
unsigned v = todo.back();
todo.pop_back();
for (auto& e1: pg->out(s))
{
unsigned i = 0;
for (auto& e2: pg->out(e1.dst))
// Env edge -> keep all
for (auto &e1: arena->out(v))
{
bool self_loop = false;
if (e1.dst == s || e2.dst == e1.dst)
self_loop = true;
if (self_loop || strat.at(e1.dst) == i)
assert(w.at(e1.dst));
if (!unsplit)
{
bdd out = bdd_satoneset(e2.cond, all_outputs, bddfalse);
if (pg2aut[e2.dst] == -1)
if (pg2aut[e1.dst] == unseen_mark)
pg2aut[e1.dst] = aut->new_state();
aut->new_edge(pg2aut[v], pg2aut[e1.dst], e1.cond,
keep_acc ? e1.acc : spot::acc_cond::mark_t({}));
}
// Player strat
auto &e2 = arena->edge_storage(s[e1.dst]);
if (pg2aut[e2.dst] == unseen_mark)
{
pg2aut[e2.dst] = aut->new_state();
todo.push_back(e2.dst);
}
aut->new_edge(pg2aut[s], pg2aut[e2.dst],
(e1.cond & out), {});
break;
}
++i;
}
if (leave_choice)
// Leave the choice
out = e2.cond;
else
// Save only one letter
out = bdd_satoneset(e2.cond, all_outputs, bddfalse);
aut->new_edge(unsplit ? pg2aut[v] : pg2aut[e1.dst],
pg2aut[e2.dst],
unsplit ? (e1.cond & out):out,
keep_acc ? e2.acc : spot::acc_cond::mark_t({}));
}
}
aut->purge_dead_states();
aut->set_named_prop("synthesis-outputs", new bdd(all_outputs));
// If no unsplitting is demanded, it remains a two-player arena
// We do not need to track winner as this is a
// strategy automaton
if (!unsplit)
{
std::vector<bool>& sp_pg = * sp_ptr;
std::vector<bool> sp_aut(aut->num_states());
spot::strategy_t str_aut(aut->num_states());
for (unsigned i = 0; i < arena->num_states(); ++i)
{
if (pg2aut[i] == unseen_mark)
// Does not appear in strategy
continue;
sp_aut[pg2aut[i]] = sp_pg[i];
str_aut[pg2aut[i]] = s[i];
}
aut->set_named_prop("state-player",
new std::vector<bool>(std::move(sp_aut)));
aut->set_named_prop("state-winner",
new spot::region_t(aut->num_states(), true));
aut->set_named_prop("strategy",
new spot::strategy_t(std::move(str_aut)));
}
return aut;
}
......@@ -523,22 +573,22 @@ namespace
if (want_time)
sw.start();
auto solution = parity_game_solve(dpa);
bool player1winning = solve_parity_game(dpa);
if (want_time)
solve_time = sw.stop();
if (verbose)
std::cerr << "parity game solved in " << solve_time << " seconds\n";
nb_states_parity_game = dpa->num_states();
timer.stop();
if (solution.player_winning_at(1, dpa->get_init_state_number()))
if (player1winning)
{
std::cout << "REALIZABLE\n";
if (!opt_real)
{
if (want_time)
sw.start();
auto strat_aut =
strat_to_aut(dpa, solution.winning_strategy[1], all_outputs);
auto strat_aut = apply_strategy(dpa, all_outputs,
true, false, true);
if (want_time)
strat2aut_time = sw.stop();
......
......@@ -1145,6 +1145,8 @@ Here is a list of named properties currently used inside Spot:
| ~simulated-states~ | ~std::vector<unsigned>~ | map states of the original automaton to states if the current automaton in the result of simulation-based reductions |
| ~state-names~ | ~std::vector<std::string>~ | vector naming each state of the automaton, for display purpose |
| ~state-player~ | ~std::vector<bool>~ | the automaton represents a two-player game, and the vector gives the player (0 or 1) associated to each state |
| ~state-winner~ | ~std::vector<bool>~ | vector indicating the player (0 or 1) winning from this state |
| ~strategy~ | ~std::vector<unsigned>~ | vector representing the memoryless strategy of the players in a parity game. The value corrsponds to the edge number of the transition to take. |
| ~synthesis-outputs~ | ~bdd~ | conjunction of controllable atomic propositions (used by ~print_aiger()~ to determine which propositions should be encoded as outputs of the circuit) |
Objects referenced via named properties are automatically destroyed
......
......@@ -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
{
std::vector<bool> has_winner_;
std::vector<bool> winner_;
inline bool operator()(unsigned v, bool p)
{
// returns true if player p wins v
// false otherwise
if (!has_winner_[v])
return false;
return winner_[v] == p;
}
inline void set(unsigned v, bool p)
{
has_winner_[v] = true;
winner_[v] = p;
}
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)
{
info.is_empty = false;
unsigned this_par = e.acc.max_set() - 1;
if (this_par <= max_par)
{
info.all_parities.insert(this_par);
multi_edge = true;
}
}
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;
}
// 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
{
strategy_t strategy;
std::set<unsigned> complement(subgame.begin(), subgame.end());
for (unsigned s: set)
complement.erase(s);
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]});
acc_cond::mark_t max_acc({});
for (unsigned i = 0; i <= max_parity; ++i)
max_acc.set(i);
// 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());
bool once_more;
do
{
once_more = false;
for (auto it = complement.begin(); it != complement.end();)
if (!to_add.empty())
{
grown = true;
for (unsigned v : to_add)
{
unsigned s = *it;
unsigned i = 0;
// v is winning
w_.set(v, p);
// Mark if demanded
if (acc_par)
{
assert(subgame_[v] == unseen_mark);
subgame_[v] = rd;
}
}
}
to_add.clear();
bool is_owned = (*owner)[s] == p;
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
for (const auto& e: arena->out(s))
// 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))
{
if ((e.acc & max_acc) && subgame.count(e.dst))
unsigned this_par = e.acc.max_set() - 1;
if ((subgame_[e.dst] >= rd) && (this_par <= max_par))
{
if (set.count(e.dst)
|| (attr_max && e.acc.max_set() - 1 == max_parity))
// 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)
{
strategy[s] = i;
wins = true;