// -*- coding: utf-8 -*-
// Copyright (C) 2016, 2018, 2019, 2022, 2023 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
#include
#include
#include
#include
#include
#include
namespace spot
{
namespace
{
unsigned change_set(unsigned x, unsigned num_sets,
bool change_kind, bool change_style)
{
// If the parity acceptance kind is changed,
// then the index of the sets are reversed
if (change_kind)
x = num_sets - x - 1;
// If the parity style is changed, then all the existing acceptance
// sets are shifted
x += change_style;
return x;
}
void
change_acc(twa_graph_ptr& aut, unsigned num_sets, bool change_kind,
bool change_style, bool output_max, bool input_max)
{
for (auto& e: aut->edge_vector())
if (e.acc)
{
unsigned msb = (input_max ? e.acc.max_set() : e.acc.min_set()) - 1;
e.acc = acc_cond::mark_t{change_set(msb, num_sets, change_kind,
change_style)};
}
else if (output_max && change_style)
{
// If the parity is changed, a new set is introduced.
// This new set is used to mark all the transitions of the input
// that don't belong to any acceptance sets.
e.acc.set(0);
}
}
[[noreturn]] static void
input_is_not_parity(const char* fun)
{
throw std::runtime_error(std::string(fun) +
"(): input should have "
"parity acceptance");
}
}
twa_graph_ptr
change_parity(const const_twa_graph_ptr& aut,
parity_kind kind, parity_style style)
{
return change_parity_here(make_twa_graph(aut, twa::prop_set::all()),
kind, style);
}
twa_graph_ptr
change_parity_here(twa_graph_ptr aut, parity_kind kind, parity_style style)
{
bool current_max;
bool current_odd;
if (!aut->acc().is_parity(current_max, current_odd, true))
input_is_not_parity("change_parity");
auto old_num_sets = aut->num_sets();
bool output_max = true;
switch (kind)
{
case parity_kind_max:
output_max = true;
break;
case parity_kind_min:
output_max = false;
break;
case parity_kind_same:
output_max = current_max;
break;
case parity_kind_any:
// If we need to change the style we may change the kind not to
// introduce new accset.
output_max = (((style == parity_style_odd && !current_odd)
|| (style == parity_style_even && current_odd))
&& old_num_sets % 2 == 0) != current_max;
break;
}
bool change_kind = current_max != output_max;
bool toggle_style = change_kind && (old_num_sets % 2 == 0);
bool output_odd = true;
switch (style)
{
case parity_style_odd:
output_odd = true;
break;
case parity_style_even:
output_odd = false;
break;
case parity_style_same:
output_odd = current_odd;
break;
case parity_style_any:
output_odd = current_odd != toggle_style;
// If we need to change the kind we may change the style not to
// introduce new accset.
break;
}
current_odd = current_odd != toggle_style;
bool change_style = false;
auto num_sets = old_num_sets;
// If the parity neeeds to be changed, then a new acceptance set is created.
// The old acceptance sets are shifted
if (output_odd != current_odd)
{
change_style = true;
++num_sets;
}
if (change_kind || change_style)
{
auto new_acc = acc_cond::acc_code::parity(output_max,
output_odd, num_sets);
aut->set_acceptance(num_sets, new_acc);
}
change_acc(aut, old_num_sets, change_kind,
change_style, output_max, current_max);
return aut;
}
twa_graph_ptr
cleanup_parity(const const_twa_graph_ptr& aut, bool keep_style)
{
auto result = make_twa_graph(aut, twa::prop_set::all());
return cleanup_parity_here(result, keep_style);
}
twa_graph_ptr
cleanup_parity_here(twa_graph_ptr aut, bool keep_style)
{
unsigned num_sets = aut->num_sets();
if (num_sets == 0)
return aut;
bool current_max;
bool current_odd;
if (!aut->acc().is_parity(current_max, current_odd, true))
input_is_not_parity("cleanup_parity");
// Gather all the used colors, while leaving only one per edge.
auto used_in_aut = acc_cond::mark_t();
acc_cond::mark_t allsets = aut->acc().all_sets();
if (current_max)
for (auto& t: aut->edges())
{
if (auto maxset = (t.acc & allsets).max_set())
{
t.acc = acc_cond::mark_t{maxset - 1};
used_in_aut |= t.acc;
}
else
{
t.acc = acc_cond::mark_t{};
}
}
else
for (auto& t: aut->edges())
{
t.acc = (t.acc & allsets).lowest();
used_in_aut |= t.acc;
}
if (used_in_aut)
{
if (current_max)
// If max even or max odd: if 0 is not used, we can remove 1, if
// 2 is not used, we can remove 3, etc.
// This is obvious from the encoding:
// max odd n = ... Inf(3) | (Fin(2) & (Inf(1) | Fin(0)))
// max even n = ... Fin(3) & (Inf(2) | (Fin(1) & Inf(0)))
{
unsigned n = 0;
while (n + 1 < num_sets && !used_in_aut.has(n))
{
used_in_aut.clear(n + 1);
n += 2;
}
}
else
// min even and min odd simply work the other way around:
// min even 4 = Inf(0) | (Fin(1) & (Inf(2) | Fin(3)))
// min odd 4 = Fin(0) & (Inf(1) | (Fin(2) & Inf(3)))
{
int n = num_sets - 1;
while (n >= 1 && !used_in_aut.has(n))
{
used_in_aut.clear(n - 1);
n -= 2;
}
}
}
// If no color needed in the automaton, exit early.
if (!used_in_aut)
{
if ((current_max && current_odd)
|| (!current_max && current_odd == (num_sets & 1)))
aut->set_acceptance(0, acc_cond::acc_code::t());
else
aut->set_acceptance(0, acc_cond::acc_code::f());
for (auto& e: aut->edges())
e.acc = {};
return aut;
}
// Renumber colors. Two used colors separated by a unused color
// can be merged.
std::vector rename(num_sets);
int prev_used = -1;
bool change_style = false;
unsigned new_index = 0;
for (auto i = 0U; i < num_sets; ++i)
if (used_in_aut.has(i))
{
if (prev_used == -1)
{
if (i & 1)
{
if (keep_style)
new_index = 1;
else
change_style = true;
}
}
else if ((i + prev_used) & 1)
++new_index;
rename[i] = new_index;
prev_used = i;
}
assert(prev_used >= 0);
// Update all colors according to RENAME.
// Using max_set or min_set makes no difference since
// there is now at most one color per edge.
for (auto& t: aut->edges())
{
acc_cond::mark_t m = t.acc & used_in_aut;
unsigned color = m.max_set();
if (color)
t.acc = acc_cond::mark_t{rename[color - 1]};
else
t.acc = m;
}
unsigned new_num_sets = new_index + 1;
if (new_num_sets < num_sets)
{
auto new_acc =
acc_cond::acc_code::parity(current_max,
current_odd != change_style,
new_num_sets);
aut->set_acceptance(new_num_sets, new_acc);
}
else
{
assert(!change_style);
}
return aut;
}
twa_graph_ptr
colorize_parity(const const_twa_graph_ptr& aut, bool keep_style)
{
return colorize_parity_here(make_twa_graph(aut, twa::prop_set::all()),
keep_style);
}
twa_graph_ptr
colorize_parity_here(twa_graph_ptr aut, bool keep_style)
{
bool current_max;
bool current_odd;
if (!aut->acc().is_parity(current_max, current_odd, true))
input_is_not_parity("colorize_parity");
if (!aut->is_existential())
throw std::runtime_error
("colorize_parity_here() does not support alternation");
bool has_empty_in_scc = false;
{
scc_info si(aut, scc_info_options::NONE);
for (const auto& e: aut->edges())
if (!e.acc && si.scc_of(e.src) == si.scc_of(e.dst))
{
has_empty_in_scc = true;
break;
}
}
unsigned num_sets = aut->num_sets();
bool new_odd = current_odd;
int incr = 0;
unsigned empty = current_max ? 0 : num_sets - 1;
if (has_empty_in_scc)
{
// If the automaton has an SCC transition that belongs to no set
// (called "empty trans." below), we may need to introduce a
// new acceptance set. What to do depends on the kind
// (min/max) and style (odd/even) of parity acceptance and the
// number (n) of colors used.
//
// | kind/style | n | empty tr. | other tr. | result |
// |------------+-----+------------+-----------+--------------|
// | max odd | any | set to {0} | add 1 | max even n+1 |
// | max even | any | set to {0} | add 1 | max odd n+1 |
// | min odd | any | set to {n} | unchanged | min odd n+1 |
// | min even | any | set to {n} | unchanged | min even n+1 |
//
// In the above table, the "max" cases both change their style
// We may want to add a second acceptance set to keep the
// style:
//
// | kind/style | n | empty tr. | other tr. | result |
// |------------+-----+------------+-----------+--------------|
// | max odd | any | set to {1} | add 2 | max odd n+2 |
// | max even | any | set to {1} | add 2 | max even n+2 |
if (current_max)
{
incr = 1 + keep_style;
num_sets += incr;
new_odd = current_odd == keep_style;
empty = keep_style;
}
else
{
empty = num_sets++;
}
auto new_acc =
acc_cond::acc_code::parity(current_max, new_odd, num_sets);
aut->set_acceptance(num_sets, new_acc);
}
if (current_max)
{
--incr;
for (auto& e: aut->edges())
{
auto maxset = e.acc.max_set();
e.acc = acc_cond::mark_t{maxset ? maxset + incr : empty};
}
}
else
{
for (auto& e: aut->edges())
e.acc = e.acc ? e.acc.lowest() : acc_cond::mark_t{empty};
}
return aut;
}
reduce_parity_data::reduce_parity_data(const const_twa_graph_ptr& aut,
bool layered)
{
if (!aut->acc().is_parity(parity_max, parity_odd, true))
input_is_not_parity("reduce_parity_data");
if (!aut->is_existential())
throw std::runtime_error
("reduce_parity_data() does not support alternation");
unsigned num_sets = aut->num_sets();
// The algorithm assumes "max odd" or "max even" parity. "min"
// parity is handled by converting it to "max" while the algorithm
// reads the automaton, and then back to "min" when it writes it.
//
// The main idea of the algorithm is to refine the SCCs of the
// automaton as will peel the parity layers one by one, starting
// with the maximum color.
//
// In the following tree, assume Sᵢ denotes SCCs and t. denotes a
// trivial SCC. Let's assume our original graph is made of one
// SCC S₁. In each SCC, we "peel the maximum layer", i.e., we
// remove the edges with the maximum colors. Doing so, we may
// introduce more sub-SCCs, in which we do this process
// recursively.
//
// S₁
// |
// max=4
// ╱ ╲
// S₂ S₃
// | |
// max=2 max=1
// ╱ ╲ |
// S₄ t. t.
// |
// max=0
// |
// t.
//
// Now the trick assign the same colors to all leaves, and then
// compress consecutive layers with identical parity. Let S₁[3]
// denote the transitions with color 3 in SCC S₁, let C(S₁[3])
// denote the new color we will assign to those sets.
//
// Assume we decide C(t.)=k=2n, this is arbitrary and imaginary
// (as there are no transitions to color in t.)
//
// Then
// C(S₄[0])=k because 0 and C(t.)=k have same parity
// C(S₂[2])=k because 2 and max(C(S₄[0]),C(t.)) have same parity
// C(S₃[1])=k+1 because 1 and C(t.)=k have different parity
// C(S₁[4])=k+2 because 4 and max(C(S₂[2]),C(S₃[1])) have different parity
// So in the case, the resulting automaton would use 3 colors, k,k+1,k+2.
//
// If we do the same computation with C(t.)=k=2n+1, the result is
// better:
// C(S₄[0])=k+1 because 0 and C(t.)=k have different parity
// C(S₂[2])=k+1 because 2 and max(C(S₄[0]),C(t.)) have same parity
// C(S₃[1])=k because 1 and C(t.)=k have same parity
// C(S₁[4])=k+1 because 4 and max(C(S₂[2]),C(S₃[1])) have same
// Here only two colors are needed.
//
// So the following code evaluates those two possible scenarios,
// using k=0 or k=1.
//
// -2 means the edge was never assigned a color.
unsigned evs = aut->edge_vector().size();
piprime1.resize(evs, -2); // k=1
piprime2.resize(evs, -2); // k=0
bool sba = aut->prop_state_acc().is_true();
auto rec =
[&](const scc_and_mark_filter& filter, auto& self) -> std::pair
{
scc_info si(filter);
unsigned numscc = si.scc_count();
std::pair res = {1, 0}; // k=1, k=0
for (unsigned scc = 0; scc < numscc; ++scc)
if (!si.is_trivial(scc))
{
int piri; // π(Rᵢ)
int color; // corresponding color, to deal with "min" kind
if (parity_max)
{
piri = color = si.acc_sets_of(scc).max_set() - 1;
}
else
{
color = si.acc_sets_of(scc).min_set() - 1;
if (color < 0)
color = num_sets;
// The algorithm works with max parity, so reverse
// the color range.
piri = num_sets - color - 1;
}
std::pair m;
if (piri < 0)
{
// Uncolored transition. Always odd.
m = {1, 1};
}
else
{
scc_and_mark_filter filter2(si, scc, {unsigned(color)});
m = self(filter2, self);
m.first += (piri - m.first) & 1;
m.second += (piri - m.second) & 1;
}
// Recolor edges. Depending on LAYERED we want to
// either recolor all edges for which piprime1 is -2
// (uncolored), or only the edges that we were removed
// by the previous filter.
auto coloredge = [&](auto& e) {
unsigned en = aut->edge_number(e);
bool recolor = layered
? piprime1[en] == -2
: (piri >= 0 && e.acc.has(color)) || (piri < 0 && !e.acc);
if (recolor)
{
piprime1[en] = m.first;
piprime2[en] = m.second;
}
};
if (sba)
// si.edges_of(scc) would be wrong as it can ignore
// outgoing edges removed from a previous level.
for (unsigned s: si.states_of(scc))
for (auto& e: aut->out(s))
coloredge(e);
else
for (auto& e: si.inner_edges_of(scc))
coloredge(e);
res.first = std::max(res.first, m.first);
res.second = std::max(res.second, m.second);
}
return res;
};
scc_and_mark_filter filter1(aut, {});
rec(filter1, rec);
}
twa_graph_ptr
reduce_parity(const const_twa_graph_ptr& aut, bool colored, bool layered)
{
return reduce_parity_here(make_twa_graph(aut, twa::prop_set::all()),
colored, layered);
}
twa_graph_ptr
reduce_parity_here(twa_graph_ptr aut, bool colored, bool layered)
{
unsigned num_sets = aut->num_sets();
if (!colored && num_sets == 0)
return aut;
reduce_parity_data pd(aut, layered);
// compute the used range for each vector.
int min1 = num_sets;
int max1 = -2;
for (int m : pd.piprime1)
{
if (m <= -2)
continue;
if (m < min1)
min1 = m;
if (m > max1)
max1 = m;
}
if (SPOT_UNLIKELY(max1 == -2))
{
aut->set_acceptance(0, spot::acc_cond::acc_code::f());
return aut;
}
int min2 = num_sets;
int max2 = -2;
for (int m : pd.piprime2)
{
if (m <= -2)
continue;
if (m < min2)
min2 = m;
if (m > max2)
max2 = m;
}
unsigned size1 = max1 + 1 - min1;
unsigned size2 = max2 + 1 - min2;
if (size2 < size1)
{
std::swap(size1, size2);
std::swap(min1, min2);
std::swap(pd.piprime1, pd.piprime2);
}
unsigned new_num_sets = size1;
if (pd.parity_max)
{
for (int& m : pd.piprime1)
if (m > -2)
m -= min1;
else
m = 0;
}
else
{
for (int& m : pd.piprime1)
if (m > -2)
m = new_num_sets - (m - min1) - 1;
else
m = new_num_sets - 1;
}
// The parity style changes if we shift colors by an odd number.
bool new_odd = pd.parity_odd ^ (min1 & 1);
if (!pd.parity_max)
// Switching from min<->max changes the parity style every time
// the number of colors is even. If the input was "min", we
// switched once to "max" to apply the reduction and once again
// to go back to "min". So there are two points where the
// parity style may have changed.
new_odd ^= !(num_sets & 1) ^ !(new_num_sets & 1);
if (!colored)
{
new_odd ^= pd.parity_max;
new_num_sets -= 1;
// It seems we have nothing to win by changing automata with a
// single set (unless we reduced it to 0 sets, of course).
if (new_num_sets == num_sets && num_sets == 1)
return aut;
}
aut->set_acceptance(new_num_sets,
acc_cond::acc_code::parity(pd.parity_max, new_odd,
new_num_sets));
if (colored)
for (auto& e: aut->edges())
{
unsigned n = aut->edge_number(e);
e.acc = acc_cond::mark_t({unsigned(pd.piprime1[n])});
}
else if (pd.parity_max)
for (auto& e: aut->edges())
{
unsigned n = pd.piprime1[aut->edge_number(e)];
if (n == 0)
e.acc = acc_cond::mark_t({});
else
e.acc = acc_cond::mark_t({n - 1});
}
else
for (auto& e: aut->edges())
{
unsigned n = pd.piprime1[aut->edge_number(e)];
if (n >= new_num_sets)
e.acc = acc_cond::mark_t({});
else
e.acc = acc_cond::mark_t({n});
}
// Reducing the number of colors could turn a non-weak automaton
// into a weak one
if (aut->prop_weak().is_false())
aut->prop_weak(trival::maybe());
if (aut->prop_very_weak().is_false())
aut->prop_very_weak(trival::maybe());
return aut;
}
}