diff --git a/spot/twaalgos/game.cc b/spot/twaalgos/game.cc index 17f94a7e4..faf29b2ba 100644 --- a/spot/twaalgos/game.cc +++ b/spot/twaalgos/game.cc @@ -149,73 +149,133 @@ namespace spot { public: - bool solve(const twa_graph_ptr& arena) + bool solve(const twa_graph_ptr& arena, bool solve_globally) { // 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_) + while (true) { - // Testing - // Make sure that every state that has a winner also - // belongs to a subgame - assert([&]() - { - for (unsigned i = 0; i < arena_->num_states(); ++i) - if (w_.has_winner_[i] - && (subgame_[i] == unseen_mark)) - return false; - return true; - }()); - // Useless SCCs are winning for player 0. - if (!info_->is_useful_scc(c_scc_idx_)) + // If we solve globally, + auto maybe_useful = [&](unsigned scc_idx){ + if (info_->is_useful_scc(scc_idx)) + return true; + if (!solve_globally) + return false; + // Check if we have an out-edge to a winning state + // in another scc + return std::any_of( + info_->states_of(scc_idx).begin(), + info_->states_of(scc_idx).end(), + [&](unsigned s){ + return std::any_of( + arena->out(s).begin(), + arena->out(s).end(), + [&](const auto& e){ + assert ((subgame_[e.dst] == unseen_mark) + || (info_->scc_of(e.dst) != scc_idx)); + return (info_->scc_of(e.dst) != scc_idx) + && w_.winner(e.dst); + }); + }); + }; + + for (c_scc_idx_ = 0; c_scc_idx_ < info_->scc_count(); ++c_scc_idx_) { - // This scc also gets its own subgame - ++rd_; - for (unsigned v: c_states()) - { - subgame_[v] = rd_; - 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)) + // Testing + // Make sure that every state that has a winner also + // belongs to a subgame + assert([&]() { - 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 + for (unsigned i = 0; i < arena_->num_states(); ++i) + if (w_.has_winner_[i] + && (subgame_[i] == unseen_mark)) + return false; + return true; + }()); + // Useless SCCs are winning for player 0. + if (!maybe_useful(c_scc_idx_)) { - // "Regular" solver - max_abs_par_ = *subgame_info.all_parities.begin(); - w_stack_.emplace_back(0, 0, - min_par_graph_, max_abs_par_); - zielonka(); + // This scc also gets its own subgame + ++rd_; + for (unsigned v: c_states()) + { + subgame_[v] = rd_; + 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, + min_par_graph_, max_abs_par_); + zielonka(); + } } } + if (!solve_globally) + break; + + // Update the scc_info and continue + unsigned new_init + = std::distance(subgame_.begin(), + std::find(subgame_.begin(), subgame_.end(), + unseen_mark)); + if (new_init == arena->num_states()) + break; // All states have been solved + // Compute new sccs + scc_info::edge_filter ef + = [](const twa_graph::edge_storage_t&, + unsigned dst, void* subgame){ + const auto& sg = *static_cast*>(subgame); + return sg[dst] == unseen_mark ? + scc_info::edge_filter_choice::keep : + scc_info::edge_filter_choice::ignore; + }; + info_ = std::make_unique(arena, new_init, ef, &subgame_); } - // Every state needs a winner - assert(std::all_of(w_.has_winner_.cbegin(), w_.has_winner_.cend(), - [](bool b) - { return b; })); + // Every state needs a winner (solve_globally) + // Or only those reachable + assert((solve_globally + && std::all_of(w_.has_winner_.cbegin(), w_.has_winner_.cend(), + [](bool b) { return b; })) + || (!solve_globally + && [&](){ + for (unsigned s = 0; s < arena->num_states(); ++s) + { + if ((info_->scc_of(s) != -1u) + && !w_.has_winner_.at(s)) + return false; + } + return true; + }())); // Only the states owned by the winner need a strategy assert([&]() { for (unsigned v = 0; v < arena_->num_states(); ++v) { + if (!solve_globally && (info_->scc_of(v) == -1u)) + continue; if (((*owner_ptr_)[v] == w_.winner(v)) && ((s_[v] <= 0) || (s_[v] > arena_->num_edges()))) return false; @@ -817,10 +877,10 @@ namespace spot } // anonymous - bool solve_parity_game(const twa_graph_ptr& arena) + bool solve_parity_game(const twa_graph_ptr& arena, bool solve_globally) { parity_game pg; - return pg.solve(arena); + return pg.solve(arena, solve_globally); } bool solve_game(const twa_graph_ptr& arena) diff --git a/spot/twaalgos/game.hh b/spot/twaalgos/game.hh index dbaccce75..d4937e46c 100644 --- a/spot/twaalgos/game.hh +++ b/spot/twaalgos/game.hh @@ -70,13 +70,19 @@ namespace spot /// This computes the winning strategy and winning region using /// Zielonka's recursive algorithm. \cite zielonka.98.tcs /// + /// By default only a 'local' strategy is computed: + /// Only the part of the arena reachable from the init state is considered. + /// If you want to compute a strategy for ALL states, set + /// \a solve_globally to true + /// /// Also includes some inspiration from Oink. /// \cite vandijk.18.tacas /// /// Returns the player winning in the initial state, and sets /// the state-winner and strategy named properties. SPOT_API - bool solve_parity_game(const twa_graph_ptr& arena); + bool solve_parity_game(const twa_graph_ptr& arena, + bool solve_globally = false); /// \ingroup games /// \brief Solve a safety game. diff --git a/tests/python/games.ipynb b/tests/python/games.ipynb index a6168b07e..9ec8bb76e 100644 --- a/tests/python/games.ipynb +++ b/tests/python/games.ipynb @@ -897,7 +897,7 @@ "\n" ], "text/plain": [ - " *' at 0x7feee9b0ebb0> >" + " *' at 0x7fcbe436f840> >" ] }, "execution_count": 8, @@ -1224,7 +1224,7 @@ "\n" ], "text/plain": [ - " *' at 0x7fef001c87b0> >" + " *' at 0x7fcbe436e9a0> >" ] }, "execution_count": 11, @@ -1663,11 +1663,1143 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "# Global vs local solver\n", + "\n", + "The parity game solver now supports \"local\" and global solutions.\n", + "\n", + "- \"Local\" solutions are the ones computed so far. A strategy is only computed for the part of the automaton that is rachable from the initial state\n", + "- Global solutions can now be obtained by setting the argument \"solve_globally\" to true. In this case a strategy will be computed even for states not reachable in the original automaton.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inf(\n", + "\n", + ")\n", + "[Büchi]\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "0\n", + "\n", + "\n", + "\n", + "I->0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->2\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2->3\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "3->4\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4->0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "5->0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "5->6\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "6->5\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "8\n", + "\n", + "\n", + "\n", + "7->8\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "9\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "7->9\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "10\n", + "\n", + "10\n", + "\n", + "\n", + "\n", + "8->10\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "9->10\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "11\n", + "\n", + "11\n", + "\n", + "\n", + "\n", + "10->11\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "11->7\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "12\n", + "\n", + "12\n", + "\n", + "\n", + "\n", + "12->7\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "13\n", + "\n", + "13\n", + "\n", + "\n", + "\n", + "12->13\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "13->12\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "14\n", + "\n", + "14\n", + "\n", + "\n", + "\n", + "15\n", + "\n", + "15\n", + "\n", + "\n", + "\n", + "14->15\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "16\n", + "\n", + "16\n", + "\n", + "\n", + "\n", + "14->16\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "17\n", + "\n", + "17\n", + "\n", + "\n", + "\n", + "15->17\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "16->17\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "18\n", + "\n", + "18\n", + "\n", + "\n", + "\n", + "17->18\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "18->14\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "19\n", + "\n", + "19\n", + "\n", + "\n", + "\n", + "19->14\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "20\n", + "\n", + "20\n", + "\n", + "\n", + "\n", + "19->20\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "20->19\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + " *' at 0x7fcbe4382370> >" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "arena = spot.make_twa_graph()\n", + "\n", + "arena.new_states(3*7)\n", + "arena.set_buchi()\n", + "\n", + "edges = [(0,1), (0,2), (1,3), (2,3), (3,4), (4,0), (5,0), (5,6), (6,5)]\n", + "\n", + "for src, dst in edges:\n", + " arena.new_edge(src, dst, bddtrue, [0] if src == 4 else [])\n", + " arena.new_edge(src + 7, dst + 7, bddtrue, [0] if src == 4 else [])\n", + " arena.new_edge(src + 14, dst + 14, bddtrue, [0] if src == 6 else [])\n", + "\n", + "arena.set_state_players(3*[False, True, True, False, True, True, False])\n", + "arena" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, 7, 10, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inf(\n", + "\n", + ")\n", + "[Büchi]\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "0\n", + "\n", + "\n", + "\n", + "I->0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->2\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2->3\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "3->4\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4->0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "5->0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "5->6\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "6->5\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "8\n", + "\n", + "\n", + "\n", + "7->8\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "9\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "7->9\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "10\n", + "\n", + "10\n", + "\n", + "\n", + "\n", + "8->10\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "9->10\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "11\n", + "\n", + "11\n", + "\n", + "\n", + "\n", + "10->11\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "11->7\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "12\n", + "\n", + "12\n", + "\n", + "\n", + "\n", + "12->7\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "13\n", + "\n", + "13\n", + "\n", + "\n", + "\n", + "12->13\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "13->12\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "14\n", + "\n", + "14\n", + "\n", + "\n", + "\n", + "15\n", + "\n", + "15\n", + "\n", + "\n", + "\n", + "14->15\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "16\n", + "\n", + "16\n", + "\n", + "\n", + "\n", + "14->16\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "17\n", + "\n", + "17\n", + "\n", + "\n", + "\n", + "15->17\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "16->17\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "18\n", + "\n", + "18\n", + "\n", + "\n", + "\n", + "17->18\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "18->14\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "19\n", + "\n", + "19\n", + "\n", + "\n", + "\n", + "19->14\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "20\n", + "\n", + "20\n", + "\n", + "\n", + "\n", + "19->20\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "20->19\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + " *' at 0x7fcbe4382370> >" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 1) Solving the game locally\n", + "# Unreachable parts are ignored, all of them are \"won\" by the env,\n", + "# the associated strategy is the 0 edges indicating no strategy\n", + "spot.solve_parity_game(arena)\n", + "spot.highlight_strategy(arena)\n", + "print(arena.get_strategy())\n", + "arena" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, 7, 10, 0, 16, 19, 0, 0, 8, 11, 0, 17, 20, 0, 3, 0, 0, 15, 0, 24, 0)\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inf(\n", + "\n", + ")\n", + "[Büchi]\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "0\n", + "\n", + "\n", + "\n", + "I->0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->2\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2->3\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "3->4\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "4->0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "5->0\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "5->6\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "6->5\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "8\n", + "\n", + "\n", + "\n", + "7->8\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "9\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "7->9\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "10\n", + "\n", + "10\n", + "\n", + "\n", + "\n", + "8->10\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "9->10\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "11\n", + "\n", + "11\n", + "\n", + "\n", + "\n", + "10->11\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "11->7\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n", + "12\n", + "\n", + "12\n", + "\n", + "\n", + "\n", + "12->7\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "13\n", + "\n", + "13\n", + "\n", + "\n", + "\n", + "12->13\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "13->12\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "14\n", + "\n", + "14\n", + "\n", + "\n", + "\n", + "15\n", + "\n", + "15\n", + "\n", + "\n", + "\n", + "14->15\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "16\n", + "\n", + "16\n", + "\n", + "\n", + "\n", + "14->16\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "17\n", + "\n", + "17\n", + "\n", + "\n", + "\n", + "15->17\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "16->17\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "18\n", + "\n", + "18\n", + "\n", + "\n", + "\n", + "17->18\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "18->14\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "19\n", + "\n", + "19\n", + "\n", + "\n", + "\n", + "19->14\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "20\n", + "\n", + "20\n", + "\n", + "\n", + "\n", + "19->20\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "20->19\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + " *' at 0x7fcbe4382370> >" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 1) Solving the game globally\n", + "# The whole automaton is considered in this case\n", + "spot.solve_parity_game(arena, True)\n", + "spot.highlight_strategy(arena)\n", + "print(arena.get_strategy())\n", + "arena" + ] } ], "metadata": { @@ -1686,7 +2818,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.10.7" } }, "nbformat": 4,