ltlsynt: add option --global-equivalence
Fixes issue #529. * spot/tl/apcollect.hh, spot/tl/apcollect.cc (collect_equivalent_literals): New function. * python/spot/impl.i: Adjust. * spot/tl/formula.hh, spot/tl/formula.cc (formula_ptr_less_than_bool_first): New comparison function. * spot/twaalgos/aiger.hh, spot/twaalgos/aiger.cc: Adjust to deal with equivalent assignments. * bin/ltlsynt.cc: Implement the new option. * tests/core/ltlsynt.test: Adjust test cases.
This commit is contained in:
parent
c016f561fa
commit
9bf1edd80d
10 changed files with 515 additions and 70 deletions
18
NEWS
18
NEWS
|
|
@ -16,13 +16,25 @@ New in spot 2.11.6.dev (not yet released)
|
||||||
will replace boolean subformulas by fresh atomic propositions even
|
will replace boolean subformulas by fresh atomic propositions even
|
||||||
if those subformulas share atomic propositions.
|
if those subformulas share atomic propositions.
|
||||||
|
|
||||||
- ltlsynt will no check for output atomic propositions that always
|
- ltlsynt will now check for atomic propositions that always have
|
||||||
have the same polarity in the specification. When this happens,
|
the same polarity in the specification. When this happens, these
|
||||||
these output APs are replaced by true or false before running the
|
output APs are replaced by true or false before running the
|
||||||
synthesis pipeline, and the resulting game, Mealy machine, or
|
synthesis pipeline, and the resulting game, Mealy machine, or
|
||||||
Aiger circuit is eventually patched to include that constant
|
Aiger circuit is eventually patched to include that constant
|
||||||
output. This can be disabled with --polarity=no.
|
output. This can be disabled with --polarity=no.
|
||||||
|
|
||||||
|
- ltlsynt will now check for atomic propositions that are specified
|
||||||
|
as equivalent. When this is detected, equivalent atomic
|
||||||
|
propositions are replaced by one representative of their class, to
|
||||||
|
limit the number of different APs processed by the synthesis
|
||||||
|
pipeline. The resulting game, Mealy machine, or Aiger circuit is
|
||||||
|
eventually patched to include the removed APs. This optimization
|
||||||
|
can be disabled with --global-equivalence=no. As an exception, an
|
||||||
|
equivalence between input and output signals (such as G(in<->out))
|
||||||
|
will be ignored if ltlsynt is configured to output a game (because
|
||||||
|
patching the game a posteriori is cumbersome if the equivalence
|
||||||
|
concerns different players).
|
||||||
|
|
||||||
Library:
|
Library:
|
||||||
|
|
||||||
- The following new trivial simplifications have been implemented for SEREs:
|
- The following new trivial simplifications have been implemented for SEREs:
|
||||||
|
|
|
||||||
185
bin/ltlsynt.cc
185
bin/ltlsynt.cc
|
|
@ -31,6 +31,7 @@
|
||||||
#include <spot/misc/bddlt.hh>
|
#include <spot/misc/bddlt.hh>
|
||||||
#include <spot/misc/escape.hh>
|
#include <spot/misc/escape.hh>
|
||||||
#include <spot/misc/timer.hh>
|
#include <spot/misc/timer.hh>
|
||||||
|
#include <spot/priv/robin_hood.hh>
|
||||||
#include <spot/tl/formula.hh>
|
#include <spot/tl/formula.hh>
|
||||||
#include <spot/tl/apcollect.hh>
|
#include <spot/tl/apcollect.hh>
|
||||||
#include <spot/twa/twagraph.hh>
|
#include <spot/twa/twagraph.hh>
|
||||||
|
|
@ -52,6 +53,7 @@ enum
|
||||||
OPT_DECOMPOSE,
|
OPT_DECOMPOSE,
|
||||||
OPT_DOT,
|
OPT_DOT,
|
||||||
OPT_FROM_PGAME,
|
OPT_FROM_PGAME,
|
||||||
|
OPT_GEQUIV,
|
||||||
OPT_HIDE,
|
OPT_HIDE,
|
||||||
OPT_INPUT,
|
OPT_INPUT,
|
||||||
OPT_OUTPUT,
|
OPT_OUTPUT,
|
||||||
|
|
@ -105,6 +107,9 @@ static const argp_option options[] =
|
||||||
{ "polarity", OPT_POLARITY, "yes|no", 0,
|
{ "polarity", OPT_POLARITY, "yes|no", 0,
|
||||||
"whether to remove atomic propositions that always have the same "
|
"whether to remove atomic propositions that always have the same "
|
||||||
"polarity in the formula to speed things up (enabled by default)", 0 },
|
"polarity in the formula to speed things up (enabled by default)", 0 },
|
||||||
|
{ "global-equivalence", OPT_GEQUIV, "yes|no", 0,
|
||||||
|
"whether to remove atomic propositions that are always equivalent to "
|
||||||
|
"another one (enabled by default)", 0 },
|
||||||
{ "simplify", OPT_SIMPLIFY, "no|bisim|bwoa|sat|bisim-sat|bwoa-sat", 0,
|
{ "simplify", OPT_SIMPLIFY, "no|bisim|bwoa|sat|bisim-sat|bwoa-sat", 0,
|
||||||
"simplification to apply to the controller (no) nothing, "
|
"simplification to apply to the controller (no) nothing, "
|
||||||
"(bisim) bisimulation-based reduction, (bwoa) bisimulation-based "
|
"(bisim) bisimulation-based reduction, (bwoa) bisimulation-based "
|
||||||
|
|
@ -241,6 +246,7 @@ static bool decompose_values[] =
|
||||||
ARGMATCH_VERIFY(decompose_args, decompose_values);
|
ARGMATCH_VERIFY(decompose_args, decompose_values);
|
||||||
bool opt_decompose_ltl = true;
|
bool opt_decompose_ltl = true;
|
||||||
bool opt_polarity = true;
|
bool opt_polarity = true;
|
||||||
|
bool opt_gequiv = true;
|
||||||
|
|
||||||
static const char* const simplify_args[] =
|
static const char* const simplify_args[] =
|
||||||
{
|
{
|
||||||
|
|
@ -265,6 +271,11 @@ ARGMATCH_VERIFY(simplify_args, simplify_values);
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
static bool want_game()
|
||||||
|
{
|
||||||
|
return opt_print_pg || opt_print_hoa;
|
||||||
|
}
|
||||||
|
|
||||||
auto str_tolower = [] (std::string s)
|
auto str_tolower = [] (std::string s)
|
||||||
{
|
{
|
||||||
std::transform(s.begin(), s.end(), s.begin(),
|
std::transform(s.begin(), s.end(), s.begin(),
|
||||||
|
|
@ -272,12 +283,17 @@ namespace
|
||||||
return s;
|
return s;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
dispatch_print_hoa(spot::twa_graph_ptr& game,
|
dispatch_print_hoa(spot::twa_graph_ptr& game,
|
||||||
const std::vector<std::string>* input_aps = nullptr,
|
const std::vector<std::string>* input_aps = nullptr,
|
||||||
const spot::relabeling_map* rm = nullptr)
|
const spot::relabeling_map* rm = nullptr)
|
||||||
{
|
{
|
||||||
if (rm && !rm->empty()) // Add any AP we removed
|
// Add any AP we removed. This is a game, so player moves are
|
||||||
|
// separated. Consequently at this point we cannot deal with
|
||||||
|
// removed signals such as "o1 <-> i2": if the game has to be
|
||||||
|
// printed, we can only optimize for signals such as o1 <-> o2.
|
||||||
|
if (rm && !rm->empty())
|
||||||
{
|
{
|
||||||
assert(input_aps);
|
assert(input_aps);
|
||||||
auto& sp = spot::get_state_players(game);
|
auto& sp = spot::get_state_players(game);
|
||||||
|
|
@ -294,6 +310,15 @@ namespace
|
||||||
add &= bdd_ithvar(i);
|
add &= bdd_ithvar(i);
|
||||||
else if (v.is_ff())
|
else if (v.is_ff())
|
||||||
add &= bdd_nithvar(i);
|
add &= bdd_nithvar(i);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bdd bv;
|
||||||
|
if (v.is(spot::op::ap))
|
||||||
|
bv = bdd_ithvar(game->register_ap(v.ap_name()));
|
||||||
|
else // Not Ap
|
||||||
|
bv = bdd_nithvar(game->register_ap(v[0].ap_name()));
|
||||||
|
add &= bdd_biimp(bdd_ithvar(i), bv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (auto& e: game->edges())
|
for (auto& e: game->edges())
|
||||||
if (sp[e.src])
|
if (sp[e.src])
|
||||||
|
|
@ -417,18 +442,27 @@ namespace
|
||||||
return;
|
return;
|
||||||
if (first_dap)
|
if (first_dap)
|
||||||
{
|
{
|
||||||
*gi->verbose_stream << ("the following APs are polarized, "
|
*gi->verbose_stream
|
||||||
"they can be replaced by constants:\n");
|
<< "the following signals can be temporarily removed:\n";
|
||||||
first_dap = false;
|
first_dap = false;
|
||||||
}
|
}
|
||||||
*gi->verbose_stream << " " << p << " := " << rm[p] <<'\n';
|
*gi->verbose_stream << " " << p << " := " << rm[p] <<'\n';
|
||||||
};
|
};
|
||||||
spot::formula oldf;
|
spot::formula oldf;
|
||||||
if (opt_polarity)
|
if (opt_polarity || opt_gequiv)
|
||||||
|
{
|
||||||
|
robin_hood::unordered_set<spot::formula> ap_inputs;
|
||||||
|
for (const std::string& ap: input_aps)
|
||||||
|
ap_inputs.insert(spot::formula::ap(ap));
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
bool rm_has_new_terms = false;
|
bool rm_has_new_terms = false;
|
||||||
std::set<spot::formula> lits = spot::collect_litterals(f);
|
oldf = f;
|
||||||
|
|
||||||
|
if (opt_polarity)
|
||||||
|
{
|
||||||
|
std::set<spot::formula> lits = spot::collect_literals(f);
|
||||||
for (const std::string& ap: output_aps)
|
for (const std::string& ap: output_aps)
|
||||||
{
|
{
|
||||||
spot::formula pos = spot::formula::ap(ap);
|
spot::formula pos = spot::formula::ap(ap);
|
||||||
|
|
@ -437,7 +471,8 @@ namespace
|
||||||
bool has_neg = lits.find(neg) != lits.end();
|
bool has_neg = lits.find(neg) != lits.end();
|
||||||
if (has_pos ^ has_neg)
|
if (has_pos ^ has_neg)
|
||||||
{
|
{
|
||||||
rm[pos] = has_pos ? spot::formula::tt() : spot::formula::ff();
|
rm[pos] =
|
||||||
|
has_pos ? spot::formula::tt() : spot::formula::ff();
|
||||||
rm_has_new_terms = true;
|
rm_has_new_terms = true;
|
||||||
display_ap(pos);
|
display_ap(pos);
|
||||||
}
|
}
|
||||||
|
|
@ -450,20 +485,113 @@ namespace
|
||||||
bool has_neg = lits.find(neg) != lits.end();
|
bool has_neg = lits.find(neg) != lits.end();
|
||||||
if (has_pos ^ has_neg)
|
if (has_pos ^ has_neg)
|
||||||
{
|
{
|
||||||
rm[pos] = has_neg ? spot::formula::tt() : spot::formula::ff();
|
rm[pos] =
|
||||||
|
has_neg ? spot::formula::tt() : spot::formula::ff();
|
||||||
rm_has_new_terms = true;
|
rm_has_new_terms = true;
|
||||||
display_ap(pos);
|
display_ap(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
oldf = f;
|
|
||||||
if (rm_has_new_terms)
|
if (rm_has_new_terms)
|
||||||
{
|
{
|
||||||
f = spot::relabel_apply(f, &rm);
|
f = spot::relabel_apply(f, &rm);
|
||||||
if (gi->verbose_stream)
|
if (gi->verbose_stream)
|
||||||
*gi->verbose_stream << "new formula: " << f << '\n';
|
*gi->verbose_stream << "new formula: " << f << '\n';
|
||||||
|
rm_has_new_terms = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opt_gequiv)
|
||||||
|
{
|
||||||
|
// check for equivalent terms
|
||||||
|
spot::formula_ptr_less_than_bool_first cmp;
|
||||||
|
for (std::vector<spot::formula>& equiv:
|
||||||
|
spot::collect_equivalent_literals(f))
|
||||||
|
{
|
||||||
|
// For each set of equivalent literals, we want to
|
||||||
|
// pick a representative. That representative
|
||||||
|
// should be an input if one of the literal is an
|
||||||
|
// input. (If we have two inputs or more, the
|
||||||
|
// formula is not realizable.)
|
||||||
|
spot::formula repr = nullptr;
|
||||||
|
bool repr_is_input = false;
|
||||||
|
spot::formula input_seen = nullptr;
|
||||||
|
for (spot::formula lit: equiv)
|
||||||
|
{
|
||||||
|
spot::formula ap = lit;
|
||||||
|
if (ap.is(spot::op::Not))
|
||||||
|
ap = ap[0];
|
||||||
|
if (ap_inputs.find(ap) != ap_inputs.end())
|
||||||
|
{
|
||||||
|
if (input_seen)
|
||||||
|
{
|
||||||
|
// ouch! we have two equivalent inputs.
|
||||||
|
// This means the formula is simply
|
||||||
|
// unrealizable. Make it false for the
|
||||||
|
// rest of the algorithm.
|
||||||
|
f = spot::formula::ff();
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
input_seen = lit;
|
||||||
|
// Normally, we want the input to be the
|
||||||
|
// representative. However as a special
|
||||||
|
// case, we ignore the input literal from
|
||||||
|
// the set if we are asked to print a
|
||||||
|
// game. Fixing the game to add a i<->o
|
||||||
|
// equivalence would require more code
|
||||||
|
// than I care to write.
|
||||||
|
//
|
||||||
|
// So if the set was {i,o1,o2}, instead
|
||||||
|
// of the desirable
|
||||||
|
// o1 := i
|
||||||
|
// o2 := i
|
||||||
|
// we only do
|
||||||
|
// o2 := o1
|
||||||
|
// when printing games.
|
||||||
|
if (!want_game())
|
||||||
|
{
|
||||||
|
repr_is_input = true;
|
||||||
|
repr = lit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!repr_is_input && (!repr || cmp(ap, repr)))
|
||||||
|
repr = lit;
|
||||||
|
}
|
||||||
|
// now map equivalent each atomic proposition to the
|
||||||
|
// representative
|
||||||
|
spot::formula not_repr = spot::formula::Not(repr);
|
||||||
|
for (spot::formula lit: equiv)
|
||||||
|
{
|
||||||
|
// input or representative are not removed
|
||||||
|
// (we have repr != input_seen either when input_seen
|
||||||
|
// is nullptr, or if want_game is true)
|
||||||
|
if (lit == repr || lit == input_seen)
|
||||||
|
continue;
|
||||||
|
if (lit.is(spot::op::Not))
|
||||||
|
{
|
||||||
|
spot::formula ap = lit[0];
|
||||||
|
rm[ap] = not_repr;
|
||||||
|
display_ap(ap);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rm[lit] = repr;
|
||||||
|
display_ap(lit);
|
||||||
|
}
|
||||||
|
rm_has_new_terms = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rm_has_new_terms)
|
||||||
|
{
|
||||||
|
f = spot::relabel_apply(f, &rm);
|
||||||
|
if (gi->verbose_stream)
|
||||||
|
*gi->verbose_stream << "new formula: " << f << '\n';
|
||||||
|
rm_has_new_terms = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (oldf != f);
|
while (oldf != f);
|
||||||
|
done:
|
||||||
|
/* can't have a label followed by closing brace */;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<spot::formula> sub_form;
|
std::vector<spot::formula> sub_form;
|
||||||
std::vector<std::set<spot::formula>> sub_outs;
|
std::vector<std::set<spot::formula>> sub_outs;
|
||||||
|
|
@ -510,8 +638,6 @@ namespace
|
||||||
assert((sub_form.size() == sub_outs.size())
|
assert((sub_form.size() == sub_outs.size())
|
||||||
&& (sub_form.size() == sub_outs_str.size()));
|
&& (sub_form.size() == sub_outs_str.size()));
|
||||||
|
|
||||||
const bool want_game = opt_print_pg || opt_print_hoa;
|
|
||||||
|
|
||||||
std::vector<spot::twa_graph_ptr> arenas;
|
std::vector<spot::twa_graph_ptr> arenas;
|
||||||
|
|
||||||
auto sub_f = sub_form.begin();
|
auto sub_f = sub_form.begin();
|
||||||
|
|
@ -528,7 +654,7 @@ namespace
|
||||||
};
|
};
|
||||||
// If we want to print a game,
|
// If we want to print a game,
|
||||||
// we never use the direct approach
|
// we never use the direct approach
|
||||||
if (!want_game && opt_bypass)
|
if (!want_game() && opt_bypass)
|
||||||
m_like =
|
m_like =
|
||||||
spot::try_create_direct_strategy(*sub_f, *sub_o, *gi, !opt_real);
|
spot::try_create_direct_strategy(*sub_f, *sub_o, *gi, !opt_real);
|
||||||
|
|
||||||
|
|
@ -555,7 +681,7 @@ namespace
|
||||||
assert((spptr->at(arena->get_init_state_number()) == false)
|
assert((spptr->at(arena->get_init_state_number()) == false)
|
||||||
&& "Env needs first turn");
|
&& "Env needs first turn");
|
||||||
}
|
}
|
||||||
if (want_game)
|
if (want_game())
|
||||||
{
|
{
|
||||||
dispatch_print_hoa(arena, &input_aps, &rm);
|
dispatch_print_hoa(arena, &input_aps, &rm);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -615,7 +741,7 @@ namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we only wanted to print the game we are done
|
// If we only wanted to print the game we are done
|
||||||
if (want_game)
|
if (want_game())
|
||||||
{
|
{
|
||||||
safe_tot_time();
|
safe_tot_time();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -681,6 +807,7 @@ namespace
|
||||||
if (!rm.empty()) // Add any AP we removed
|
if (!rm.empty()) // Add any AP we removed
|
||||||
{
|
{
|
||||||
bdd add = bddtrue;
|
bdd add = bddtrue;
|
||||||
|
bdd additional_outputs = bddtrue;
|
||||||
for (auto [k, v]: rm)
|
for (auto [k, v]: rm)
|
||||||
{
|
{
|
||||||
int i = tot_strat->register_ap(k);
|
int i = tot_strat->register_ap(k);
|
||||||
|
|
@ -689,15 +816,39 @@ namespace
|
||||||
!= input_aps.end())
|
!= input_aps.end())
|
||||||
continue;
|
continue;
|
||||||
if (v.is_tt())
|
if (v.is_tt())
|
||||||
add &= bdd_ithvar(i);
|
{
|
||||||
|
bdd bv = bdd_ithvar(i);
|
||||||
|
additional_outputs &= bv;
|
||||||
|
add &= bv;
|
||||||
|
}
|
||||||
else if (v.is_ff())
|
else if (v.is_ff())
|
||||||
|
{
|
||||||
|
additional_outputs &= bdd_ithvar(i);
|
||||||
add &= bdd_nithvar(i);
|
add &= bdd_nithvar(i);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bdd left = bdd_ithvar(i); // this is necessarily an output
|
||||||
|
additional_outputs &= left;
|
||||||
|
bool pos = v.is(spot::op::ap);
|
||||||
|
const std::string apname =
|
||||||
|
pos ? v.ap_name() : v[0].ap_name();
|
||||||
|
bdd right = bdd_ithvar(tot_strat->register_ap(apname));
|
||||||
|
// right might be an input
|
||||||
|
if (std::find(input_aps.begin(), input_aps.end(), apname)
|
||||||
|
== input_aps.end())
|
||||||
|
additional_outputs &= right;
|
||||||
|
if (pos)
|
||||||
|
add &= bdd_biimp(left, right);
|
||||||
|
else
|
||||||
|
add &= bdd_xor(left, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (auto& e: tot_strat->edges())
|
for (auto& e: tot_strat->edges())
|
||||||
e.cond &= add;
|
e.cond &= add;
|
||||||
set_synthesis_outputs(tot_strat,
|
set_synthesis_outputs(tot_strat,
|
||||||
get_synthesis_outputs(tot_strat)
|
get_synthesis_outputs(tot_strat)
|
||||||
& bdd_support(add));
|
& additional_outputs);
|
||||||
}
|
}
|
||||||
printer.print(tot_strat, timer_printer_dummy);
|
printer.print(tot_strat, timer_printer_dummy);
|
||||||
}
|
}
|
||||||
|
|
@ -1052,6 +1203,10 @@ parse_opt(int key, char *arg, struct argp_state *)
|
||||||
case OPT_FROM_PGAME:
|
case OPT_FROM_PGAME:
|
||||||
jobs.emplace_back(arg, job_type::AUT_FILENAME);
|
jobs.emplace_back(arg, job_type::AUT_FILENAME);
|
||||||
break;
|
break;
|
||||||
|
case OPT_GEQUIV:
|
||||||
|
opt_gequiv = XARGMATCH("--global-equivalence", arg,
|
||||||
|
decompose_args, decompose_values);
|
||||||
|
break;
|
||||||
case OPT_HIDE:
|
case OPT_HIDE:
|
||||||
show_status = false;
|
show_status = false;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -524,6 +524,7 @@ namespace std {
|
||||||
%template(vectorint) vector<int>;
|
%template(vectorint) vector<int>;
|
||||||
%template(pair_formula_vectorstring) pair<spot::formula, vector<string>>;
|
%template(pair_formula_vectorstring) pair<spot::formula, vector<string>>;
|
||||||
%template(atomic_prop_set) set<spot::formula>;
|
%template(atomic_prop_set) set<spot::formula>;
|
||||||
|
%template(vectorofvectorofformulas) vector<vector<spot::formula>>;
|
||||||
%template(setunsigned) set<unsigned>;
|
%template(setunsigned) set<unsigned>;
|
||||||
%template(relabeling_map) map<spot::formula, spot::formula>;
|
%template(relabeling_map) map<spot::formula, spot::formula>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,13 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include <map>
|
||||||
#include <spot/tl/apcollect.hh>
|
#include <spot/tl/apcollect.hh>
|
||||||
#include <spot/twa/twa.hh>
|
#include <spot/twa/twa.hh>
|
||||||
|
#include <spot/twa/twagraph.hh>
|
||||||
#include <spot/twa/bdddict.hh>
|
#include <spot/twa/bdddict.hh>
|
||||||
|
#include <spot/twaalgos/hoa.hh>
|
||||||
|
#include <spot/twaalgos/sccinfo.hh>
|
||||||
|
|
||||||
namespace spot
|
namespace spot
|
||||||
{
|
{
|
||||||
|
|
@ -64,7 +68,7 @@ namespace spot
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic_prop_set collect_litterals(formula f)
|
atomic_prop_set collect_literals(formula f)
|
||||||
{
|
{
|
||||||
atomic_prop_set res;
|
atomic_prop_set res;
|
||||||
|
|
||||||
|
|
@ -131,4 +135,150 @@ namespace spot
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<spot::formula>>
|
||||||
|
collect_equivalent_literals(formula f)
|
||||||
|
{
|
||||||
|
std::map<spot::formula, unsigned> l2s;
|
||||||
|
// represent the implication graph as a twa_graph so we cab reuse
|
||||||
|
// scc_info. Literals can be converted to states using the l2s
|
||||||
|
// map.
|
||||||
|
twa_graph_ptr igraph = make_twa_graph(make_bdd_dict());
|
||||||
|
|
||||||
|
auto state_of = [&](formula a)
|
||||||
|
{
|
||||||
|
auto [it, b] = l2s.insert({a, 0});
|
||||||
|
if (b)
|
||||||
|
it->second = igraph->new_state();
|
||||||
|
return it->second;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto implies = [&](formula a, formula b)
|
||||||
|
{
|
||||||
|
unsigned pos_a = state_of(a);
|
||||||
|
unsigned neg_a = state_of(formula::Not(a));
|
||||||
|
unsigned pos_b = state_of(b);
|
||||||
|
unsigned neg_b = state_of(formula::Not(b));
|
||||||
|
igraph->new_edge(pos_a, pos_b, bddtrue);
|
||||||
|
igraph->new_edge(neg_b, neg_a, bddtrue);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto collect = [&](formula f, bool in_g, auto self)
|
||||||
|
{
|
||||||
|
switch (f.kind())
|
||||||
|
{
|
||||||
|
case op::ff:
|
||||||
|
case op::tt:
|
||||||
|
case op::eword:
|
||||||
|
case op::ap:
|
||||||
|
case op::UConcat:
|
||||||
|
case op::Not:
|
||||||
|
case op::NegClosure:
|
||||||
|
case op::NegClosureMarked:
|
||||||
|
case op::U:
|
||||||
|
case op::R:
|
||||||
|
case op::W:
|
||||||
|
case op::M:
|
||||||
|
case op::EConcat:
|
||||||
|
case op::EConcatMarked:
|
||||||
|
case op::X:
|
||||||
|
case op::F:
|
||||||
|
case op::Closure:
|
||||||
|
case op::OrRat:
|
||||||
|
case op::AndRat:
|
||||||
|
case op::AndNLM:
|
||||||
|
case op::Concat:
|
||||||
|
case op::Fusion:
|
||||||
|
case op::Star:
|
||||||
|
case op::FStar:
|
||||||
|
case op::first_match:
|
||||||
|
case op::strong_X:
|
||||||
|
return;
|
||||||
|
case op::Xor:
|
||||||
|
if (in_g && f[0].is_literal() && f[1].is_literal())
|
||||||
|
{
|
||||||
|
implies(f[0], formula::Not(f[1]));
|
||||||
|
implies(formula::Not(f[0]), f[1]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case op::Equiv:
|
||||||
|
if (in_g && f[0].is_literal() && f[1].is_literal())
|
||||||
|
{
|
||||||
|
implies(f[0], f[1]);
|
||||||
|
implies(formula::Not(f[0]), formula::Not(f[1]));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case op::Implies:
|
||||||
|
if (in_g && f[0].is_literal() && f[1].is_literal())
|
||||||
|
implies(f[0], f[1]);
|
||||||
|
return;
|
||||||
|
case op::G:
|
||||||
|
self(f[0], true, self);
|
||||||
|
return;
|
||||||
|
case op::Or:
|
||||||
|
if (f.size() == 2 && f[0].is_literal() && f[1].is_literal())
|
||||||
|
implies(formula::Not(f[0]), f[1]);
|
||||||
|
return;
|
||||||
|
case op::And:
|
||||||
|
for (formula c: f)
|
||||||
|
self(c, in_g, self);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
collect(f, false, collect);
|
||||||
|
|
||||||
|
scc_info si(igraph, scc_info_options::PROCESS_UNREACHABLE_STATES);
|
||||||
|
|
||||||
|
// print_hoa(std::cerr, igraph);
|
||||||
|
|
||||||
|
// Build sets of equivalent literals.
|
||||||
|
unsigned nscc = si.scc_count();
|
||||||
|
std::vector<std::vector<spot::formula>> scc(nscc);
|
||||||
|
for (auto [f, i]: l2s)
|
||||||
|
scc[si.scc_of(i)].push_back(f);
|
||||||
|
|
||||||
|
// For each set, we will decide if we keep it or not.
|
||||||
|
std::vector<bool> keep(nscc, true);
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < nscc; ++i)
|
||||||
|
{
|
||||||
|
if (keep[i] == false)
|
||||||
|
continue;
|
||||||
|
// We don't keep trivial SCCs
|
||||||
|
if (scc[i].size() <= 1)
|
||||||
|
{
|
||||||
|
keep[i] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Each SCC will appear twice. Because if {a,!b,c,!d,!e} are
|
||||||
|
// equivalent literals, then so are {!a,b,!c,d,e}. We will
|
||||||
|
// keep the SCC with the fewer negation if we can.
|
||||||
|
unsigned neg_count = 0;
|
||||||
|
for (formula f: scc[i])
|
||||||
|
{
|
||||||
|
SPOT_ASSUME(f != nullptr);
|
||||||
|
neg_count += f.is(op::Not);
|
||||||
|
}
|
||||||
|
if (neg_count > scc[i].size()/2)
|
||||||
|
{
|
||||||
|
keep[i] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We will keep the current SCC. Just
|
||||||
|
// mark the dual one for removal.
|
||||||
|
keep[si.scc_of(state_of(formula::Not(*scc[i].begin())))] = false;
|
||||||
|
}
|
||||||
|
// purge unwanted SCCs
|
||||||
|
unsigned j = 0;
|
||||||
|
for (unsigned i = 0; i < nscc; ++i)
|
||||||
|
{
|
||||||
|
if (keep[i] == false)
|
||||||
|
continue;
|
||||||
|
if (i > j)
|
||||||
|
scc[j] = std::move(scc[i]);
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
scc.resize(j);
|
||||||
|
return scc;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
#include <spot/tl/formula.hh>
|
#include <spot/tl/formula.hh>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
#include <bddx.h>
|
#include <bddx.h>
|
||||||
#include <spot/twa/fwd.hh>
|
#include <spot/twa/fwd.hh>
|
||||||
|
|
||||||
|
|
@ -60,14 +61,22 @@ namespace spot
|
||||||
atomic_prop_collect_as_bdd(formula f, const twa_ptr& a);
|
atomic_prop_collect_as_bdd(formula f, const twa_ptr& a);
|
||||||
|
|
||||||
|
|
||||||
/// \brief Collect the litterals occuring in f
|
/// \brief Collect the literals occuring in f
|
||||||
///
|
///
|
||||||
/// This function records each atomic proposition occurring in f
|
/// This function records each atomic proposition occurring in f
|
||||||
/// along with the polarity of its occurrence. For instance if the
|
/// along with the polarity of its occurrence. For instance if the
|
||||||
/// formula is `G(a -> b) & X(!b & c)`, then this will output `{!a,
|
/// formula is `G(a -> b) & X(!b & c)`, then this will output `{!a,
|
||||||
/// b, !b, c}`.
|
/// b, !b, c}`.
|
||||||
SPOT_API
|
SPOT_API
|
||||||
atomic_prop_set collect_litterals(formula f);
|
atomic_prop_set collect_literals(formula f);
|
||||||
|
|
||||||
|
/// \brief Collect equivalent APs
|
||||||
|
///
|
||||||
|
/// Looks for patterns like `...&G(...&(x->y)&...)&...` or
|
||||||
|
/// other forms of constant implications, then build a graph
|
||||||
|
/// of implications to compute equivalence classes of literals.
|
||||||
|
SPOT_API
|
||||||
|
std::vector<std::vector<spot::formula>>
|
||||||
|
collect_equivalent_literals(formula f);
|
||||||
/// @}
|
/// @}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// -*- coding: utf-8 -*-
|
// -*- coding: utf-8 -*-
|
||||||
// Copyright (C) 2015-2019, 2021, 2022 Laboratoire de Recherche et
|
// Copyright (C) 2015-2019, 2021, 2022, 2023 Laboratoire de Recherche et
|
||||||
// Développement de l'Epita (LRDE).
|
// Développement de l'Epita (LRDE).
|
||||||
//
|
//
|
||||||
// This file is part of Spot, a model checking library.
|
// This file is part of Spot, a model checking library.
|
||||||
|
|
@ -2071,4 +2071,11 @@ namespace spot
|
||||||
{
|
{
|
||||||
return print_psl(os, f);
|
return print_psl(os, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
formula_ptr_less_than_bool_first::operator()(const formula& left,
|
||||||
|
const formula& right) const
|
||||||
|
{
|
||||||
|
return operator()(left.ptr_, right.ptr_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -652,6 +652,8 @@ namespace spot
|
||||||
SPOT_API
|
SPOT_API
|
||||||
int atomic_prop_cmp(const fnode* f, const fnode* g);
|
int atomic_prop_cmp(const fnode* f, const fnode* g);
|
||||||
|
|
||||||
|
class SPOT_API formula;
|
||||||
|
|
||||||
struct formula_ptr_less_than_bool_first
|
struct formula_ptr_less_than_bool_first
|
||||||
{
|
{
|
||||||
bool
|
bool
|
||||||
|
|
@ -718,6 +720,9 @@ namespace spot
|
||||||
right->dump(ord);
|
right->dump(ord);
|
||||||
return old.str() < ord.str();
|
return old.str() < ord.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SPOT_API bool
|
||||||
|
operator()(const formula& left, const formula& right) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SWIG
|
#endif // SWIG
|
||||||
|
|
@ -726,6 +731,7 @@ namespace spot
|
||||||
/// \brief Main class for temporal logic formula
|
/// \brief Main class for temporal logic formula
|
||||||
class SPOT_API formula final
|
class SPOT_API formula final
|
||||||
{
|
{
|
||||||
|
friend struct formula_ptr_less_than_bool_first;
|
||||||
const fnode* ptr_;
|
const fnode* ptr_;
|
||||||
public:
|
public:
|
||||||
/// \brief Create a formula from an fnode.
|
/// \brief Create a formula from an fnode.
|
||||||
|
|
|
||||||
|
|
@ -1986,6 +1986,21 @@ namespace
|
||||||
for (unsigned i = 0; i < n_outs; ++i)
|
for (unsigned i = 0; i < n_outs; ++i)
|
||||||
circuit.set_output(i, bdd2var_min(out[i], out_dc[i]));
|
circuit.set_output(i, bdd2var_min(out[i], out_dc[i]));
|
||||||
// Add the unused propositions
|
// Add the unused propositions
|
||||||
|
//
|
||||||
|
// RM contains assignments like
|
||||||
|
// out1 := 1
|
||||||
|
// out2 := 0
|
||||||
|
// out3 := in1
|
||||||
|
// out4 := !out3
|
||||||
|
// but it is possible that the rhs could refer to a variable
|
||||||
|
// that is not yet defined because of the ordering. For
|
||||||
|
// this reason, the first pass will store signals it could not
|
||||||
|
// complete in the POSTPONE vector.
|
||||||
|
//
|
||||||
|
// In that vector, (u,v,b) means that output u should be mapped to
|
||||||
|
// the same formula as output v, possibly negated (if b).
|
||||||
|
std::vector<std::tuple<int, int, bool>> postpone;
|
||||||
|
|
||||||
const unsigned n_outs_all = output_names_all.size();
|
const unsigned n_outs_all = output_names_all.size();
|
||||||
for (unsigned i = n_outs; i < n_outs_all; ++i)
|
for (unsigned i = n_outs; i < n_outs_all; ++i)
|
||||||
if (rm)
|
if (rm)
|
||||||
|
|
@ -2003,10 +2018,61 @@ namespace
|
||||||
circuit.set_output(i, circuit.aig_false());
|
circuit.set_output(i, circuit.aig_false());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
formula repr = to->second;
|
||||||
|
bool neg_repr = false;
|
||||||
|
if (repr.is(op::Not))
|
||||||
|
{
|
||||||
|
neg_repr = true;
|
||||||
|
repr = repr[0];
|
||||||
|
}
|
||||||
|
// is repr an input?
|
||||||
|
if (auto it = std::find(input_names_all.begin(),
|
||||||
|
input_names_all.end(),
|
||||||
|
repr.ap_name());
|
||||||
|
it != input_names_all.end())
|
||||||
|
{
|
||||||
|
unsigned ivar =
|
||||||
|
circuit.input_var(it - input_names_all.begin(),
|
||||||
|
neg_repr);
|
||||||
|
circuit.set_output(i, ivar);
|
||||||
|
}
|
||||||
|
// is repr an output?
|
||||||
|
else if (auto it = std::find(output_names_all.begin(),
|
||||||
|
output_names_all.end(),
|
||||||
|
repr.ap_name());
|
||||||
|
it != output_names_all.end())
|
||||||
|
{
|
||||||
|
unsigned outnum = it - output_names_all.begin();
|
||||||
|
unsigned outvar = circuit.output(outnum);
|
||||||
|
if (outvar == -1u)
|
||||||
|
postpone.emplace_back(i, outnum, neg_repr);
|
||||||
|
else
|
||||||
|
circuit.set_output(i, outvar + neg_repr);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
circuit.set_output(i, circuit.aig_false());
|
circuit.set_output(i, circuit.aig_false());
|
||||||
|
unsigned postponed = postpone.size();
|
||||||
|
while (postponed)
|
||||||
|
{
|
||||||
|
unsigned postponed_again = 0;
|
||||||
|
for (auto [u, v, b]: postpone)
|
||||||
|
{
|
||||||
|
unsigned outvar = circuit.output(v);
|
||||||
|
if (outvar == -1u)
|
||||||
|
++postponed_again;
|
||||||
|
else
|
||||||
|
circuit.set_output(u, outvar + b);
|
||||||
|
}
|
||||||
|
if (postponed_again >= postponed)
|
||||||
|
throw std::runtime_error("aiger encoding bug: "
|
||||||
|
"postponed output shunts not decreasing");
|
||||||
|
postponed = postponed_again;
|
||||||
|
}
|
||||||
for (unsigned i = 0; i < n_latches; ++i)
|
for (unsigned i = 0; i < n_latches; ++i)
|
||||||
circuit.set_next_latch(i, bdd2var_min(latch[i], bddfalse));
|
circuit.set_next_latch(i, bdd2var_min(latch[i], bddfalse));
|
||||||
return circuit_ptr;
|
return circuit_ptr;
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,15 @@ namespace spot
|
||||||
[](unsigned o){return o == -1u; }));
|
[](unsigned o){return o == -1u; }));
|
||||||
return outputs_;
|
return outputs_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// \brief return the variable associated to output \a num
|
||||||
|
///
|
||||||
|
/// This will be equal to -1U if aig::set_output() hasn't been called.
|
||||||
|
unsigned output(unsigned num) const
|
||||||
|
{
|
||||||
|
return outputs_[num];
|
||||||
|
}
|
||||||
|
|
||||||
/// \brief Get the set of output names
|
/// \brief Get the set of output names
|
||||||
const std::vector<std::string>& output_names() const
|
const std::vector<std::string>& output_names() const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
diff outx exp
|
diff outx exp
|
||||||
|
|
||||||
cat >exp <<EOF
|
cat >exp <<EOF
|
||||||
the following APs are polarized, they can be replaced by constants:
|
the following signals can be temporarily removed:
|
||||||
i0 := 1
|
i0 := 1
|
||||||
i2 := 1
|
i2 := 1
|
||||||
new formula: GFi1 -> G(i1 <-> o0)
|
new formula: GFi1 -> G(i1 <-> o0)
|
||||||
|
|
@ -638,16 +638,15 @@ grep "one of --ins or --outs" stderr
|
||||||
# Try to find a direct strategy for GFa <-> GFb and a direct strategy for
|
# Try to find a direct strategy for GFa <-> GFb and a direct strategy for
|
||||||
# Gc
|
# Gc
|
||||||
cat >exp <<EOF
|
cat >exp <<EOF
|
||||||
there are 2 subformulas
|
the following signals can be temporarily removed:
|
||||||
|
c := d
|
||||||
|
new formula: GFa <-> GFb
|
||||||
|
there are 1 subformulas
|
||||||
trying to create strategy directly for GFa <-> GFb
|
trying to create strategy directly for GFa <-> GFb
|
||||||
tanslating formula done in X seconds
|
tanslating formula done in X seconds
|
||||||
direct strategy was found.
|
direct strategy was found.
|
||||||
direct strat has 1 states, 2 edges and 0 colors
|
direct strat has 1 states, 2 edges and 0 colors
|
||||||
simplification took X seconds
|
simplification took X seconds
|
||||||
trying to create strategy directly for G(c <-> d)
|
|
||||||
direct strategy was found.
|
|
||||||
direct strat has 1 states, 1 edges and 0 colors
|
|
||||||
simplification took X seconds
|
|
||||||
EOF
|
EOF
|
||||||
ltlsynt -f '(GFa <-> GFb) && (G(c <-> d))' --outs=b,c --verbose 2> out
|
ltlsynt -f '(GFa <-> GFb) && (G(c <-> d))' --outs=b,c --verbose 2> out
|
||||||
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
|
|
@ -665,7 +664,8 @@ direct strategy was found.
|
||||||
direct strat has 1 states, 2 edges and 0 colors
|
direct strat has 1 states, 2 edges and 0 colors
|
||||||
simplification took X seconds
|
simplification took X seconds
|
||||||
EOF
|
EOF
|
||||||
ltlsynt -f "$f" --outs=b,c --verbose --decompose=0 --verify 2> out
|
ltlsynt -f "$f" --outs=b,c --verbose --decompose=0 \
|
||||||
|
--global-equiv=no --verify 2> out
|
||||||
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
diff outx exp
|
diff outx exp
|
||||||
done
|
done
|
||||||
|
|
@ -673,7 +673,7 @@ done
|
||||||
# # Ltlsynt should be able to detect that G(a&c) is not input-complete so it is
|
# # Ltlsynt should be able to detect that G(a&c) is not input-complete so it is
|
||||||
# # impossible to find a strategy.
|
# # impossible to find a strategy.
|
||||||
cat >exp <<EOF
|
cat >exp <<EOF
|
||||||
the following APs are polarized, they can be replaced by constants:
|
the following signals can be temporarily removed:
|
||||||
c := 1
|
c := 1
|
||||||
new formula: (GFb <-> GFa) & Ga
|
new formula: (GFb <-> GFa) & Ga
|
||||||
trying to create strategy directly for (GFb <-> GFa) & Ga
|
trying to create strategy directly for (GFb <-> GFa) & Ga
|
||||||
|
|
@ -763,7 +763,7 @@ sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
diff outx exp
|
diff outx exp
|
||||||
|
|
||||||
cat >exp <<EOF
|
cat >exp <<EOF
|
||||||
the following APs are polarized, they can be replaced by constants:
|
the following signals can be temporarily removed:
|
||||||
b := 1
|
b := 1
|
||||||
a := 1
|
a := 1
|
||||||
new formula: x & y
|
new formula: x & y
|
||||||
|
|
@ -1023,7 +1023,7 @@ sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
diff outx exp
|
diff outx exp
|
||||||
|
|
||||||
cat >exp <<EOF
|
cat >exp <<EOF
|
||||||
the following APs are polarized, they can be replaced by constants:
|
the following signals can be temporarily removed:
|
||||||
o2 := 1
|
o2 := 1
|
||||||
new formula: GFi <-> GFo1
|
new formula: GFi <-> GFo1
|
||||||
there are 1 subformulas
|
there are 1 subformulas
|
||||||
|
|
@ -1038,6 +1038,36 @@ ltlsynt -f "G(o1|o2) & (GFi <-> GFo1)" --outs="o1,o2" --verbose\
|
||||||
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
diff outx exp
|
diff outx exp
|
||||||
|
|
||||||
|
|
||||||
|
# Test the loop around polarity/global-equiv
|
||||||
|
cat >exp <<EOF
|
||||||
|
the following signals can be temporarily removed:
|
||||||
|
r3 := 1
|
||||||
|
new formula: G(i <-> o) & G(o <-> o2) & G(!o | !o3) & GFo3
|
||||||
|
o := i
|
||||||
|
o2 := i
|
||||||
|
new formula: GFo3 & G(!i | !o3)
|
||||||
|
i := 1
|
||||||
|
new formula: GFo3 & G!o3
|
||||||
|
there are 1 subformulas
|
||||||
|
trying to create strategy directly for GFo3 & G!o3
|
||||||
|
direct strategy might exist but was not found.
|
||||||
|
translating formula done in X seconds
|
||||||
|
automaton has 1 states and 0 colors
|
||||||
|
LAR construction done in X seconds
|
||||||
|
DPA has 1 states, 0 colors
|
||||||
|
split inputs and outputs done in X seconds
|
||||||
|
automaton has 3 states
|
||||||
|
solving game with acceptance: co-Büchi
|
||||||
|
game solved in X seconds
|
||||||
|
UNREALIZABLE
|
||||||
|
EOF
|
||||||
|
ltlsynt -f 'G(o<->i) & G(o2 <-> o) & G(!o | !o3) & G(r3 -> Fo3)' \
|
||||||
|
--ins=i,r3 --verbose 2>out 1>&2 && exit 1
|
||||||
|
sed 's/ [0-9.e-]* seconds/ X seconds/g' out > outx
|
||||||
|
diff outx exp
|
||||||
|
|
||||||
|
|
||||||
# Test --dot and --hide-status
|
# Test --dot and --hide-status
|
||||||
ltlsynt -f 'i <-> Fo' --ins=i --aiger --dot | grep arrowhead=dot
|
ltlsynt -f 'i <-> Fo' --ins=i --aiger --dot | grep arrowhead=dot
|
||||||
ltlsynt -f 'i <-> Fo' --ins=i --print-game-hoa --dot | grep 'shape="diamond"'
|
ltlsynt -f 'i <-> Fo' --ins=i --print-game-hoa --dot | grep 'shape="diamond"'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue