// -*- coding: utf-8 -*- // Copyright (C) 2017, 2018, 2020 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 . #include "config.h" #include #include #include #include namespace spot { namespace { static const std::vector* ensure_game(const const_twa_graph_ptr& arena, const char* fnname) { auto owner = arena->get_named_prop>("state-player"); if (!owner) throw std::runtime_error (std::string(fnname) + ": automaton should define \"state-player\""); if (owner->size() != arena->num_states()) throw std::runtime_error (std::string(fnname) + ": \"state-player\" should have " "as many states as the automaton"); return owner; } static const std::vector* ensure_parity_game(const const_twa_graph_ptr& arena, const char* fnname) { bool max, odd; arena->acc().is_parity(max, odd, true); if (!(max && odd)) throw std::runtime_error (std::string(fnname) + ": arena must have max-odd acceptance condition"); for (const auto& e : arena->edges()) if (!e.acc) throw std::runtime_error (std::string(fnname) + ": arena must be colorized"); return ensure_game(arena, fnname); } // Internal structs // winning regions for env and player struct winner_t { std::vector has_winner_; std::vector 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> 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) { // 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); // The strategy for player 0 is to take the first // available edge. if ((*owner_ptr_)[v] == false) for (const auto &e : arena_->out(v)) { s_[v] = arena_->edge_number(e); break; } } 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(); 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("state-winner"); strategy_t &s = *arena->get_or_set_named_prop("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 &c_states() { assert(info_); return info_->states_of(c_scc_idx_); } void set_up(const twa_graph_ptr &arena) { owner_ptr_ = ensure_parity_game(arena, "solve_parity_game()"); 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(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 { 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 (info_->scc_of(e.dst) != c_scc_idx_) { // 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 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): { 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; } 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 // 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 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 // 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 } // 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(); } // 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); 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 } const unsigned unseen_mark = std::numeric_limits::max(); twa_graph_ptr arena_; const std::vector *owner_ptr_; unsigned rd_; winner_t w_; // Subgame array similar to the one from oink! std::vector 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 s_; // Informations about sccs andthe current scc std::unique_ptr 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 change_stash_; // Change recursive calls to stack std::vector 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"); unsigned ns = arena->num_states(); unsigned init = arena->get_init_state_number(); os << "parity " << ns - 1 << ";\n"; std::vector seen(ns, false); std::vector todo({init}); while (!todo.empty()) { unsigned src = todo.back(); todo.pop_back(); if (seen[src]) continue; seen[src] = true; os << src << ' '; os << arena->out(src).begin()->acc.max_set() - 1 << ' '; os << (*owner)[src] << ' '; bool first = true; for (auto& e: arena->out(src)) { if (!first) os << ','; first = false; os << e.dst; if (!seen[e.dst]) todo.push_back(e.dst); } if (src == init) os << " \"INIT\""; os << ";\n"; } } 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 ("propagate_players(): game winning condition is a tautology"); unsigned sink_env = 0; unsigned sink_con = 0; std::vector seen(arena->num_states(), false); unsigned init = arena->get_init_state_number(); std::vector todo({init}); auto owner = new std::vector(arena->num_states(), false); (*owner)[init] = first_player; while (!todo.empty()) { unsigned src = todo.back(); todo.pop_back(); seen[src] = true; bdd missing = bddtrue; for (const auto& e: arena->out(src)) { bool osrc = (*owner)[src]; if (complete0 && !osrc) missing -= e.cond; if (!seen[e.dst]) { (*owner)[e.dst] = !osrc; todo.push_back(e.dst); } else if ((*owner)[e.dst] == osrc) { delete owner; throw std::runtime_error("propagate_players(): odd cycle detected"); } } if (complete0 && !(*owner)[src] && missing != bddfalse) { if (sink_env == 0) { sink_env = arena->new_state(); sink_con = arena->new_state(); owner->push_back(true); owner->push_back(false); arena->new_edge(sink_con, sink_env, bddtrue, um.second); arena->new_edge(sink_env, sink_con, bddtrue, um.second); } arena->new_edge(src, sink_con, missing, um.second); } } arena->set_named_prop("state-player", owner); } twa_graph_ptr highlight_strategy(twa_graph_ptr& aut, int player0_color, int player1_color) { auto owner = ensure_game(aut, "highlight_strategy()"); region_t* w = aut->get_named_prop("state-winner"); strategy_t* s = aut->get_named_prop("strategy"); if (!w) throw std::runtime_error ("highlight_strategy(): " "winning region unavailable, solve the game first"); if (!s) throw std::runtime_error ("highlight_strategy(): strategy unavailable, solve the game first"); unsigned ns = aut->num_states(); auto* hl_edges = aut->get_or_set_named_prop> ("highlight-edges"); auto* hl_states = aut->get_or_set_named_prop> ("highlight-states"); if (unsigned sz = std::min(w->size(), s->size()); sz < ns) ns = sz; for (unsigned n = 0; n < ns; ++n) { 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; } void set_state_players(twa_graph_ptr arena, std::vector owners) { std::vector* owners_ptr = new std::vector(owners); set_state_players(arena, owners_ptr); } void set_state_players(twa_graph_ptr arena, std::vector* owners) { if (owners->size() != arena->num_states()) throw std::runtime_error ("set_state_players(): There must be as many owners as states"); arena->set_named_prop>("state-player", owners); } void set_state_player(twa_graph_ptr arena, unsigned state, unsigned owner) { if (state >= arena->num_states()) throw std::runtime_error("set_state_player(): invalid state number"); std::vector *owners = arena->get_named_prop> ("state-player"); if (!owners) { owners = new std::vector(arena->num_states(), false); arena->set_named_prop>("state-player", owners); } (*owners)[state] = owner; } const std::vector& get_state_players(const_twa_graph_ptr arena) { std::vector *onwers = arena->get_named_prop> ("state-player"); if (!onwers) throw std::runtime_error ("get_state_players(): state-player property not defined, not a game"); return *onwers; } unsigned get_state_player(const_twa_graph_ptr arena, unsigned state) { if (state >= arena->num_states()) throw std::runtime_error("get_state_player(): invalid state number"); std::vector* onwers = arena->get_named_prop> ("state-player"); if (!onwers) throw std::runtime_error ("get_state_player(): state-player property not defined, not a game"); return (*onwers)[state] ? 1 : 0; } }