* doc/org/tut40.org: Add a documentation on how to use games in a pratical case. In this example, we compute simulation using games. Fixes #445. * doc/Makefile.am: Add tut40.org.
12 KiB
Using games to compute simulation
This example demonstrates how to find the simulations between states using games in C++ and python. This algorithm is describe in Fair Simulation Relations, Parity Games, and State Space Reduction for Büchi Automata by R. Schuller Borbely, Kousha Etessami and Thomas Wilke.
The simulation based reduction (spot.simuation() and spot.reduce_direct_sim()) does not use this method as is it slower.
When all the moves are in the game, we check if the spoiler has a winning strategy from a pair of state. If yes, there is simulation otherwise there are no.
We will compute the simulation between 4 and 0 in the following automaton.
HOA: v1
States: 6
Start: 0
AP: 2 "a" "b"
acc-name: co-Buchi
Acceptance: 1 Fin(0)
properties: trans-labels explicit-labels state-acc very-weak
--BODY--
State: 0
[1] 1
[1] 2
State: 1 {0}
[0&1] 1
State: 2
[0] 3
State: 3
[1] 3
State: 4
[1] 5
State: 5
[0] 5
--END--
Python
First a need to create a game. The game is build by alternating the possible moves of the two players: the spoiler (even) and the duplicator (odd). The spoiler starts from its state and chose a transition and move to the destination. Then, the duplicator search for a transition from its state with a compatible condition and acceptance.
import spot
from spot import buddy
def direct_sim_game(aut, s1, s2):
if s1 >= aut.num_states() or s2 >= aut.num_states():
raise ValueError('invalid state number')
game = spot.make_twa_graph(aut.get_dict())
# The names are a pair of int. If even owns the state, the first integer
# is the number of the edge that spoiler has taken during this turn (so
# we have the current position of the spoiler (the destination of the
# edge), the condition and acceptance that duplicator must find). If
# it is an odd state, the first integer is the state where the spoiler
# is. In either case, the second is always the duplicator's state.
names = []
owners = []
# Keep the game's state from its name. We need two maps as two states can
# have the same name (but not the same owner). For example (0, 1) can
# describe a spoiler state (the edge 0, the state 1) and a duplicator
# state (the states 0 and 1).
s_orig_states = {}
d_orig_states = {}
todo = []
# Create the state (i, j) for a player if it does not exist yet and
# returns its state's number in the game.
def get_and_create_game_state(player, i, j):
orig_state = s_orig_states if player else d_orig_states
if not (i, j) in orig_state:
s = game.new_state()
names.append((i, j))
owners.append(player)
orig_state[(i, j)] = s
# If it is the duplicator turn and the state does not exist
# yet, we need to process it.
if not player:
todo.append(s)
return s
else:
return orig_state[(i, j)]
get_and_create_game_state(False, s1, s2);
(all_inf, _) = aut.get_acceptance().used_inf_fin_sets()
while todo:
cur = todo.pop()
(s_src, d_src) = names[cur]
# Spoiler starts by picking an edge from s_src. If it does not exist
# yet, we add a new edge in the game from (s_src,d_src) to
# (edge_idx,d_src).
for s_edge in aut.out(s_src):
edge_idx = aut.edge_number(s_edge)
cur2 = get_and_create_game_state(True, edge_idx, d_src)
game.new_edge(cur, cur2, buddy.bddtrue)
# Duplicator try to find an edge with the same condition from d_src.
# If it does not exist yet, it adds a new edge from
# (s_edge.dst,d_src) to (s_edge.dst, d_edge.dst).
for d_edge in aut.out(d_src):
if buddy.bdd_implies(d_edge.cond, s_edge.cond) \
and (s_edge.acc ^ all_inf).subset(d_edge.acc ^ all_inf):
cur3 = get_and_create_game_state(False, s_edge.dst, d_edge.dst)
game.new_edge(cur2, cur3, buddy.bddtrue)
spot.set_state_players(game, owners)
names = [s for s in map(str, names)]
game.set_state_names(names)
return game
Then we solve it. This game is a safety game, the odd player tries to block the even player into a dead end. Even win if it has a winning strategy, aka there is no dead end in the game.
spot.solve_safety_game(g)
Next, we get the winners for each state. We only check the even's state as it is the only ones that describe states pairs (the odd's ones describe the edge taken and a state). If odd wins from this state, then the first state simulates the second.
def compute_simulation(aut, i, j):
g = direct_sim_game(aut, i, j)
spot.solve_safety_game(g)
winners = spot.get_state_winners(g)
owners = spot.get_state_players(g)
names = g.get_state_names()
simulations = []
for i in range(0, g.num_states()):
if winners[i] and not owners[i]:
simulations.append(tuple(map(int, names[i][1:-1].split(', '))))
return simulations
print(compute_simulation(aut, 4, 0))
[(4, 0), (5, 1)]
C++
The equivalent code in C++:
#include <spot/twaalgos/game.hh>
#include <spot/twa/twagraph.hh>
spot::twa_graph_ptr direct_sim_game(spot::const_twa_graph_ptr aut,
unsigned s1, unsigned s2)
{
if (s1 >= aut->num_states() || s2 >= aut->num_states())
throw std::runtime_error("direct_sim_game(): invalid state number");
auto game = spot::make_twa_graph(aut->get_dict());
game->copy_ap_of(aut);
auto names = new std::vector<std::pair<unsigned, unsigned>>();
game->set_named_prop("product-states", names);
names->reserve(aut->num_states());
auto owners = new std::vector<bool>();
game->set_named_prop("state-player", owners);
owners->reserve(aut->num_states());
std::map<std::pair<unsigned, unsigned>, unsigned> s_orig_states;
std::map<std::pair<unsigned, unsigned>, unsigned> d_orig_states;
std::vector<unsigned> todo;
todo.reserve(aut->num_states());
auto& g = game->get_graph();
auto all_inf = aut->get_acceptance().used_inf_fin_sets().first;
auto new_state = [&](bool player, unsigned s1, unsigned s2)
{
unsigned s;
auto& m = player ? s_orig_states : d_orig_states;
auto it = std::find_if(m.begin(), m.end(), [s1, s2](auto elm)
{ return s1 == elm.first.first && s2 == elm.first.second; });
if (it == m.end())
{
s = game->new_state();
names->emplace_back(s1, s2);
owners->push_back(player);
m.insert({{s1, s2}, s});
if (!player)
todo.push_back(s);
}
else
{
s = it->second;
}
return s;
};
new_state(false, s1, s2);
while (!todo.empty())
{
unsigned cur = todo.back();
unsigned s_src = (*names)[cur].first;
unsigned d_src = (*names)[cur].second;
todo.pop_back();
for (const auto& s_edge : aut->out(s_src))
{
unsigned edge_idx = g.index_of_edge(s_edge);
unsigned cur2 = new_state(true, edge_idx, d_src);
game->new_edge(cur, cur2, bddtrue);
for (const auto& d_edge : aut->out(d_src))
{
if (bdd_implies(d_edge.cond, s_edge.cond)
&& (s_edge.acc ^ all_inf).subset(d_edge.acc ^ all_inf))
{
unsigned cur3 = new_state(false, s_edge.dst, d_edge.dst);
game->new_edge(cur2, cur3, bddtrue);
}
}
}
}
return game;
}
bool compute_simulation(spot::const_twa_graph_ptr aut,
unsigned i, unsigned j,
std::vector<bool>& can_sim)
{
auto g = direct_sim_game(aut, i, j);
bool i_simulates_j = spot::solve_safety_game(g);
std::vector<bool> winners = spot::get_state_winners(g);
std::vector<bool> owners = spot::get_state_players(g);
typedef std::vector<std::pair<unsigned, unsigned>> names_t;
auto names = *g->get_named_prop<names_t>("product-states");
unsigned n = g->num_states();
unsigned n_aut = aut->num_states();
for (unsigned i = 0; i < n; ++i)
if (winners[i] && !owners[i])
{
auto [u, v] = names[i];
can_sim[u * n_aut + v] = true;
}
return i_simulates_j;
}
<<cppCallCompute>>
4 simulates 0 5 simulates 1