Commit 0d9cc29b authored by Alexandre Duret-Lutz's avatar Alexandre Duret-Lutz
Browse files

tl: eight new simplification rules

* NEWS, doc/tl/tl.tex: Document the rules.
* spot/tl/simplify.cc: Implement them.
* tests/core/reduccmp.test: Test them.
* tests/core/det.test, tests/core/ltl2tgba2.test,
tests/python/stutter-inv.ipynb, tests/core/385.test: Adjust.
parent d244ff54
......@@ -112,13 +112,21 @@ New in spot 2.7.5.dev (not yet released)
- spot::relabel_apply() makes it easier to reverse the effect
of spot::relabel() or spot::relabel_bse() on formula.
- The LTL simplifier learned the following optional rules:
- The LTL simplifier learned the following rules:
F(G(a | Fb)) = FGa | GFb (if option "favor_event_univ")
G(F(a | Gb)) = GFa | FGb (if option "favor_event_univ")
F(G(a & Fb) = FGa & GFb (unless option "reduce_size_strictly")
G(F(a & Gb)) = GFa & FGb (unless option "reduce_size_strictly")
GF(f) = GF(dnf(f)) (unless option "reduce_size_strictly")
FG(f) = FG(cnf(f)) (unless option "reduce_size_strictly")
(f & g) R h = f R h if h implies g
(f & g) M h = f M h if h implies g
(f | g) W h = f W h if g implies h
(f | g) U h = f U h if g implies h
Gf | F(g & eventual) = f W (g & eventual) if !f implies g
Ff & G(g | universal) = f M (g | universal) if f implies !g
f U (g & eventual) = F(g & eventual) if !f implies g
f R (g | universal) = G(g | universal) if f implies !g
- cleanup_parity() and colorize_parity() were cleaned up a bit,
resulting in fewer colors used in some cases. In particular,
......
......@@ -1857,6 +1857,7 @@ are counted as one.
\text{if~} & f\simp h & & \text{~then~} & (f\U g) \U h & \equiv g \U h \\
\text{if~} & f\simp h & & \text{~then~} & (f\W g) \U h & \equiv g \U h \\
\text{if~} & g\simp h & & \text{~then~} & (f\U g) \U h & \equiv (f \U g) \OR h \\
\text{if~} & g\simp h & & \text{~then~} & (f\OR g) \U h & \equiv f \U h \\
\text{if~} & (\NOT f)\simp g & & \text{~then~} & f\W g & \equiv \1 \\
\text{if~} & \flessg & & \text{~then~} & f\W g & \equiv f \\
\text{if~} & f\simp g & & \text{~then~} & f\W g & \equiv g \\
......@@ -1868,6 +1869,7 @@ are counted as one.
\text{if~} & f\simp h & & \text{~then~} & (f\W g) \W h & \equiv g \W h \\
\text{if~} & g\simp h & & \text{~then~} & (f\W g) \W h & \equiv (f \W g) \OR h \\
\text{if~} & g\simp h & & \text{~then~} & (f\U g) \W h & \equiv (f \U g) \OR h \\
\text{if~} & g\simp h & & \text{~then~} & (f\OR g) \W h & \equiv f \W h \\
\text{if~} & \flessg & & \text{~then~} & f\R g & \equiv f \\
\text{if~} & g\simp f & & \text{~then~} & f\R g & \equiv g \\
\text{if~} & g\simp \NOT f & & \text{~then~} & f\R g & \equiv \G g \\
......@@ -1879,6 +1881,7 @@ are counted as one.
\text{if~} & h\simp f & & \text{~then~} & (f\M g) \R h & \equiv g \R h \\
\text{if~} & g\simp h & & \text{~then~} & (f\R g) \R h & \equiv (f \AND g) \R h \\
\text{if~} & g\simp h & & \text{~then~} & (f\M g) \R h & \equiv (f \AND g) \R h \\
\text{if~} & h\simp g & & \text{~then~} & (f\AND g) \R h & \equiv f \R h \\
\text{if~} & \flessg & & \text{~then~} & f\M g & \equiv f \\
\text{if~} & g\simp f & & \text{~then~} & f\M g & \equiv g \\
\text{if~} & g\simp \NOT f & & \text{~then~} & f\M g & \equiv \0 \\
......@@ -1888,6 +1891,7 @@ are counted as one.
\text{if~} & h\simp f & & \text{~then~} & (f\M g) \M h & \equiv g \M h \\
\text{if~} & h\simp f & & \text{~then~} & (f\R g) \M h & \equiv g \M h \\
\text{if~} & g\simp h & & \text{~then~} & (f\M g) \M h & \equiv (f \AND g) \M h \\
\text{if~} & h\simp g & & \text{~then~} & (f\AND g) \M h & \equiv f \M h \\
\end{alignat*}
\endgroup
......@@ -1895,6 +1899,17 @@ Many of the above rules were collected from the
literature~\cite{somenzi.00.cav,tauriainen.03.tr,babiak.12.tacas} and
sometimes generalized to support operators such as $\M$ and $\W$.
The following rules mix implication-based checks with formulas that
are pure eventualities ($e$) or that are purely universal ($u$).
\allowdisplaybreaks
\begin{alignat*}{3}
\text{if~} & (\NOT f)\simp g & & \text{~then~} & f \U (g \AND e) & \equiv \F(g \AND e) \\
\text{if~} & f\simp \NOT g & & \text{~then~} & f \R (g \OR u) & \equiv \G(g \OR e) \\
\text{if~} & (\NOT f) \simp g & & \text{~then~} & \G(f) \OR \F(g \AND e) & \equiv f \W (g \AND e) \\
\text{if~} & f \simp\NOT g & & \text{~then~} & \F(f) \AND \G(g \OR e) & \equiv f \M (g \OR e) \\
\end{alignat*}
\appendix
\chapter{Defining LTL with only one of $\U$, $\W$, $\R$, or $\M$}
\label{sec:ltl-equiv}
......
......@@ -2160,6 +2160,31 @@ namespace spot
// if c => b, then (a U c) U b = (a U c) | b
if (a.is(op::U) && c_->implication(a[1], b))
return recurse(formula::Or({a, b}));
// if g => h, then (f|g) U h = f U h
if (a.is(op::Or))
{
unsigned n = a.size();
for (unsigned child = 0; child < n; ++child)
if (c_->implication(a[child], b))
return recurse(formula::U(a.all_but(child), b));
}
// a U (b & e) = F(b & e) if !b => a
if (b.is(op::And))
for (formula c: b)
if (c.is_eventual())
{
// We know there is one pure eventuality
// formula but we might have more. So lets
// extract everything else.
vec rest;
rest.reserve(c.size());
for (formula cc: b)
if (!cc.is_eventual())
rest.emplace_back(cc);
if (c_->implication_neg(formula::And(rest), a, false))
return recurse(formula::F(b));
break;
}
break;
case op::R:
......@@ -2198,6 +2223,31 @@ namespace spot
return recurse(formula::R(ac, b));
}
}
// if h => g, then (f&g) R h = f R h
if (a.is(op::And))
{
unsigned n = a.size();
for (unsigned child = 0; child < n; ++child)
if (c_->implication(b, a[child]))
return recurse(formula::R(a.all_but(child), b));
}
// a R (b | u) = G(b | u) if b => !a
if (b.is(op::Or))
for (formula c: b)
if (c.is_universal())
{
// We know there is one purely universal
// formula but we might have more. So lets
// extract everything else.
vec rest;
rest.reserve(c.size());
for (formula cc: b)
if (!cc.is_universal())
rest.emplace_back(cc);
if (c_->implication_neg(formula::Or(rest), a, true))
return recurse(formula::G(b));
break;
}
break;
case op::W:
......@@ -2234,6 +2284,14 @@ namespace spot
// if c => b, then (a U c) W b = (a U c) | b
if (a.is(op::U, op::W) && c_->implication(a[1], b))
return recurse(formula::Or({a, b}));
// if g => h, then (f|g) W h = f M h
if (a.is(op::Or))
{
unsigned n = a.size();
for (unsigned child = 0; child < n; ++child)
if (c_->implication(a[child], b))
return recurse(formula::W(a.all_but(child), b));
}
break;
case op::M:
......@@ -2265,6 +2323,14 @@ namespace spot
return
recurse(formula::M(formula::And({a[0], a[1]}),
b));
// if h => g, then (f&g) M h = f M h
if (a.is(op::And))
{
unsigned n = a.size();
for (unsigned child = 0; child < n; ++child)
if (c_->implication(b, a[child]))
return recurse(formula::M(a.all_but(child), b));
}
break;
default:
......@@ -2680,6 +2746,7 @@ namespace spot
// F(a) & (a M b) = a M b
// F(b) & (a W b) = a U b
// F(b) & (a U b) = a U b
// F(c) & G(phi | e) = c M (phi | e) if c => !phi.
typedef std::unordered_map<formula, vec::iterator> fmap_t;
fmap_t uwmap; // associates "b" to "a U b" or "a W b"
fmap_t rmmap; // associates "a" to "a R b" or "a M b"
......@@ -2762,6 +2829,43 @@ namespace spot
assert(j->second->is(op::M));
}
}
if (opt_.synt_impl | opt_.containment_checks)
{
// if the input looks like o1|u1|u2|o2,
// return o1 | o2. The input must have at
// least on universal formula.
auto extract_not_un =
[&](formula f) {
if (f.is(op::Or))
for (auto u: f)
if (u.is_universal())
{
vec phi;
phi.reserve(f.size());
for (auto uu: f)
if (!uu.is_universal())
phi.push_back(uu);
return formula::Or(phi);
}
return formula(nullptr);
};
// F(c) & G(phi | e) = c M (phi | e) if c => !phi.
for (auto in_g = s.res_G->begin();
in_g != s.res_G->end();)
{
if (formula phi = extract_not_un(*in_g))
if (c_->implication_neg(phi, c, true))
{
s.res_other->push_back(formula::M(c,
*in_g));
in_g = s.res_G->erase(in_g);
superfluous = true;
continue;
}
++in_g;
}
}
if (superfluous)
f = nullptr;
}
......@@ -3273,6 +3377,7 @@ namespace spot
// G(a) | (a W b) = a W b
// G(b) | (a R b) = a R b.
// G(b) | (a M b) = a R b.
// G(c) | F(phi & e) = c W (phi & e) if !c => phi.
typedef std::unordered_map<formula, vec::iterator> fmap_t;
fmap_t uwmap; // associates "a" to "a U b" or "a W b"
fmap_t rmmap; // associates "b" to "a R b" or "a M b"
......@@ -3355,6 +3460,43 @@ namespace spot
assert(j->second->is(op::R));
}
}
if (opt_.synt_impl | opt_.containment_checks)
{
// if the input looks like o1&e1&e2&o2,
// return o1 & o2. The input must have at
// least on eventual formula.
auto extract_not_ev =
[&](formula f) {
if (f.is(op::And))
for (auto e: f)
if (e.is_eventual())
{
vec phi;
phi.reserve(f.size());
for (auto ee: f)
if (!ee.is_eventual())
phi.push_back(ee);
return formula::And(phi);
}
return formula(nullptr);
};
// G(c) | F(phi & e) = c W (phi & e) if !c => phi.
for (auto in_f = s.res_F->begin();
in_f != s.res_F->end();)
{
if (formula phi = extract_not_ev(*in_f))
if (c_->implication_neg(c, phi, false))
{
s.res_other->push_back(formula::W(c,
*in_f));
in_f = s.res_F->erase(in_f);
superfluous = true;
continue;
}
++in_f;
}
}
if (superfluous)
f = nullptr;
}
......
......@@ -36,7 +36,7 @@ EOF
# Some of the following are still not optimal.
cat >expected <<EOF
1,GF(!a | !d | !e)
6,G(Gc | F(a | (!c & F!e)))
6,G(Fa | (c W (!c & F!e)))
2,Ge | G(Fd & FGe & Fc)
1,F(G(Fb & FGc) | Ge)
4,G(F!d | (!c & G!b))
......
......@@ -26,7 +26,7 @@ cat >formulas <<'EOF'
1,5,X(((a & b) R (!a U !c)) R b)
1,8,XXG(Fa U Xb)
1,5,(!a M !b) W F!c
1,3,(b & Fa & GFc) R a
1,3,(b & GFc) R a
1,2,(a R (b W a)) W G(!a M (b | c))
1,11,(Fa W b) R (!a | Fc)
1,6,X(G(!a M !b) | G(a | G!a))
......
......@@ -77,7 +77,7 @@ dac-patterns,37, 4,56, 4,56, 4,56, 4,56
dac-patterns,38, 4,56, 4,56, 4,56, 4,56
dac-patterns,39, 4,112, 4,112, 4,112, 4,112
dac-patterns,40, 3,88, 3,88, 3,88, 3,88
dac-patterns,41, 6,54, 6,54, 7,56, 7,56
dac-patterns,41, 4,32, 4,32, 4,32, 4,32
dac-patterns,42, 6,96, 6,96, 6,96, 6,96
dac-patterns,43, 5,80, 5,80, 5,80, 5,80
dac-patterns,44, 10,300, 10,300, 13,372, 13,372
......
......@@ -298,17 +298,36 @@ a W ((a&b) W c), a W c
(a R b) M (c&a), b M (c&a)
(a M b) M (c&a), b M (c&a)
(a R (b&c)) R (c), (a&b&c) R c
(a M (b&c)) R (c), (a&b&c) R c
(x M y) M y, x M y
(x R y) R y, x R y
(x & y) M y, x M y
(x & y) R y, x R y
(a R (b&c)) R (c), (a&b) R c
(a M (b&c)) R (c), (a&b) R c
# not reduced
(a R (b&c)) M (c), (a R (b&c)) M (c)
(a M (b&c)) M (c), (a&b&c) M c
(a M (b&c)) M (c), (a&b) M c
(a W (c&b)) W b, (a W (c&b)) | b
(a U (c&b)) W b, (a U (c&b)) | b
(a U (c&b)) U b, (a U (c&b)) | b
# not reduced
(a W (c&b)) U b, (a W (c&b)) U b
!x U (x & Fa), F(x & Fa)
!x R (x | Ga), G(x | Ga)
!x U ((x | c) & Fa & Fb), F((x | c) & Fa & Fb)
!x R ((x & c) | Ga | Gb), G((x & c) | Ga | Gb)
G!f | F((f|g) & Fa & Fb), !f W ((f | g) & Fa & Fb)
F!f & G((f&g) | Ga | Gb), !f M ((f & g) | Ga | Gb)
(x|b) W (x|a), b W (x|a)
(x|b) U (x|a), b U (x|a)
!x U ((x | c) & Fa & Fb), F((x | c) & Fa & Fb)
!x R ((x & c) | Ga | Gb), G((x & c) | Ga | Gb)
# Eventuality and universality class reductions
Fa M b, Fa & b
GFa M b, GFa & b
......
%% Cell type:code id: tags:
``` python
import spot
spot.setup(show_default='.bn')
from IPython.display import display
```
%% Cell type:markdown id: tags:
# Stutter-invariant languages
A language $L$ is said to be _stutter-invariant_ iff $\ell_0\ldots\ell_{i-1}\ell_i\ell_{i+1}\ldots\in L \iff \ell_0\ldots\ell_{i-1}\ell_i\ell_i\ell_{i+1}\ldots\in L$, i.e., if duplicating a letter in a word or removing a duplicated letter does not change the membership of that word to $L$. These languages are also called _stutter-insensitive_. We use the adjective _sutter-sensitive_ to describe a language that is not stutter-invariant. Of course we can extend this vocabulary to LTL formulas or automata that represent stutter-invariant languages.
Stutter-invariant languages play an important role in model checking. When verifying a stutter-invariant specification against a system, we know that we have some freedom in how we discretize the time in the model: as long as we do not hide changes of model variables that are observed by the specification, we can merge multiple steps of the model. This, combined by careful analysis of actions of the model that are independent, is the basis for a set of techniques known as _partial-order reductions_ (POR) that postpone the visit of some successors in the model, because we know we can always visit them later.
%% Cell type:markdown id: tags:
## Stutter-invariant formulas
When the specification is expressed as an LTL formula, a well known way to ensure it is _stutter-invariant_ is to forbid the use of the `X` operator. Testing whether a formula is `X`-free can be done in constant time using the `is_syntactic_stutter_invariant()` method.
%% Cell type:code id: tags:
``` python
f = spot.formula('a U b')
print(f.is_syntactic_stutter_invariant())
```
%% Output
True
%% Cell type:code id: tags:
``` python
f = spot.formula('a U Xb')
print(f.is_syntactic_stutter_invariant())
```
%% Output
False
%% Cell type:markdown id: tags:
However some formula are syntactic-invariant despite their use of `X`. Spot implements some [automaton-based check](https://www.lrde.epita.fr/~adl/dl/adl/michaud.15.spin.pdf) to detect stutter-invariance reliably and efficiently. This can be tested with the `is_stutter_invariant()` function.
%% Cell type:code id: tags:
``` python
g = spot.formula('F(a & X(!a & Gb))')
print(g.is_syntactic_stutter_invariant())
print(spot.is_stutter_invariant(g))
```
%% Output
False
True
%% Cell type:markdown id: tags:
Of course this `is_stutter_invariant()` function first checks whether the formula is `X`-free before wasting time building automata, so if you want to detect stutter-invariant formulas in your model checker, this is the only function to use. Also, if you hapen to already have an automaton `aut_g` for `g`, you should pass it as a second argument to avoid it being recomputed: `spot.is_stutter_invariant(g, aut_g)`.
%% Cell type:markdown id: tags:
It is also known that any stutter-invariant LTL formula can be converted to an `X`-free LTL formula. Several proofs of that exist. Spot implements the rewriting of [K. Etessami](http://homepages.inf.ed.ac.uk/kousha/note_on_stut_tl_lpi.ps) under the name `remove_x()`. Note that the output of this function is only equivalent to its input if the latter is stutter-invariant.
%% Cell type:code id: tags:
``` python
spot.remove_x(g)
```
%% Output
$\mathsf{F} (a \land ((a \land (a \mathbin{\mathsf{U}} (\lnot a \land \mathsf{G} b)) \land ((\lnot b \mathbin{\mathsf{U}} \lnot a) \lor (b \mathbin{\mathsf{U}} \lnot a))) \lor (\lnot a \land (\lnot a \mathbin{\mathsf{U}} (a \land \lnot a \land \mathsf{G} b)) \land ((\lnot b \mathbin{\mathsf{U}} a) \lor (b \mathbin{\mathsf{U}} a))) \lor (b \land (b \mathbin{\mathsf{U}} (\lnot a \land \lnot b \land \mathsf{G} b)) \land ((\lnot a \mathbin{\mathsf{U}} \lnot b) \lor (a \mathbin{\mathsf{U}} \lnot b))) \lor (\lnot b \land (\lnot b \mathbin{\mathsf{U}} (\lnot a \land b \land \mathsf{G} b)) \land ((\lnot a \mathbin{\mathsf{U}} b) \lor (a \mathbin{\mathsf{U}} b))) \lor (\lnot a \land \mathsf{G} b \land (\mathsf{G} \lnot a \lor \mathsf{G} a) \land (\mathsf{G} b \lor \mathsf{G} \lnot b))))$
spot.formula("F(a & ((a & (a U (!a & Gb)) & ((!b U !a) | (b U !a))) | (!a & (!a U (a & !a & Gb)) & ((!b U a) | (b U a))) | (b & (b U (!a & !b & Gb)) & ((!a U !b) | (a U !b))) | (!b & (!b U (!a & b & Gb)) & ((!a U b) | (a U b))) | (!a & Gb & (G!a | Ga) & (Gb | G!b))))")
%% Cell type:markdown id: tags:
## Stutter-invariant automata
%% Cell type:markdown id: tags:
Similarly to formulas, automata use a few bits to store some known properties about themselves, like whether they represent a stutter-invariant language. This property can be checked with the `prop_stutter_invariant()` method, but that returns a `trival` instance (i.e., yes, no, or maybe). Some algorithms will update that property whenever that is cheap or expliclitely asked for. For instance `spot.translate()` only sets the property if the translated formula is `X`-free.
%% Cell type:code id: tags:
``` python
aut = spot.translate(g)
print(aut.prop_stutter_invariant())
```
%% Output
maybe
%% Cell type:markdown id: tags:
As suggested above, we can call `is_stutter_invariant()` by passing a formula and its automaton, to save on one translation. A second translation is still needed to complement the automaton.
%% Cell type:code id: tags:
``` python
print(spot.is_stutter_invariant(g, aut))
```
%% Output
True
%% Cell type:markdown id: tags:
Note that `prop_stutter_invariant()` was updated as a side-effect so that any futher call to `is_stutter_invariant()` with this automaton will be instantaneous.
%% Cell type:code id: tags:
``` python
print(aut.prop_stutter_invariant())
```
%% Output
yes
%% Cell type:markdown id: tags:
You have to be aware of this property being set in your back because if while playing with `is_stutter_invariant()` you the incorrect formula for an automaton by mistake, the automaton will have its property set incorrectly, and running `is_stutter_inariant()` with the correct formula will simply return the cached property.
In doubt, you can always reset the property as follows:
%% Cell type:code id: tags:
``` python
aut.prop_stutter_invariant(spot.trival_maybe())
print(aut.prop_stutter_invariant())
```
%% Output
maybe
%% Cell type:markdown id: tags:
In case you have an automaton for which you do not have formula, you can also use `is_stutter_invariant()` by passing this automaton as the first argument. In that case a negated automaton will be constructed by determinization. If you do happen to have a negated automaton handy, you can pass it as a second argument to avoid that.
%% Cell type:code id: tags:
``` python
a1 = spot.automaton('''HOA: v1
AP: 1 "a"
States: 2
Start: 0
Acceptance: 0 t
--BODY--
State: 0 [0] 1
State: 1 [t] 0
--END--''')
display(a1)
print(spot.is_stutter_invariant(a1))
```
%% Output
False
%% Cell type:markdown id: tags:
## Explaining why a formula is not sutter-invariant
%% Cell type:markdown id: tags:
As explained in our [Spin'15 paper](https://www.lrde.epita.fr/~adl/dl/adl/michaud.15.spin.pdf) the sutter-invariant checks are implemented using simple operators suchs as `spot.closure(aut)`, that augment the language of L by adding words that can be obtained by removing duplicated letters, and `spot.sl(aut)` or `spot.sl2(aut)` that both augment the language that L by adding words that can be obtained by duplicating letters. The default `is_stutter_invariant()` function is implemented as `spot.product(spot.closure(aut), spot.closure(neg_aut)).is_empty()`, but that is just one possible implementation selected because it was more efficient.
Using these bricks, we can modify the original algorithm so it uses a counterexample to explain why a formula is stutter-sensitive.
%% Cell type:code id: tags:
``` python
def explain_stut(f):
f = spot.formula(f)
pos = spot.translate(f)
neg = spot.translate(spot.formula.Not(f))
word = spot.product(spot.closure(pos), spot.closure(neg)).accepting_word()
if word is None:
print(f, "is stutter invariant")
return
word.simplify()
word.use_all_aps(pos.ap_vars())
waut = word.as_automaton()
if waut.intersects(pos):
acc, rej, aut = "accepted", "rejected", neg
else:
acc, rej, aut = "rejected", "accepted", pos
word2 = spot.sl2(waut).intersecting_word(aut)
word2.simplify()
print("""{} is {} by {}
but if we stutter some of its letters, we get
{} which is {} by {}""".format(word, acc, f, word2, rej, f))
```
%% Cell type:code id: tags:
``` python
explain_stut('GF(a & Xb)')
```
%% Output
cycle{!a & !b; a & b} is rejected by GF(a & Xb)
but if we stutter some of its letters, we get
cycle{!a & !b; a & b; a & b} which is accepted by GF(a & Xb)
%% Cell type:markdown id: tags:
Note that a variant of the above explanation procedure is already integerated in our [on-line LTL translator tool](https://spot.lrde.epita.fr/app/) (use the <i>study</i> tab).
%% Cell type:markdown id: tags:
## Detecting stutter-invariant states
Even if the language of an automaton is not sutter invariant, some of its states may recognize a stutter-invariant language. (We assume the language of a state is the language the automaton would have when starting from this state.)
%% Cell type:markdown id: tags:
### First example
For instance let us build a disjunction of a stutter-invariant formula and a stutter-sensitive one:
%% Cell type:code id: tags:
``` python
f1 = spot.formula('F(a & X!a & XF(b & X!b & Ga))')
f2 = spot.formula('F(a & Xa & XXa & G!b)')
f = spot.formula.Or([f1, f2])
print(spot.is_stutter_invariant(f1))
print(spot.is_stutter_invariant(f2))
print(spot.is_stutter_invariant(f))
```
%% Output
True
False
False
%% Cell type:code id: tags:
``` python
pos = spot.translate(f)
display(pos)
```
%% Output
%% Cell type:markdown id: tags:
While the automaton as a whole is stutter-sensitive, we can see that eventually we will enter a sub-automaton that is stutter-invariant.
The `stutter_invariant_states()` function returns a Boolean vector indiced by the state number. A state is marked as `True` if either its language is stutter-invariant, or if it can only be reached via a stutter-invariant state (see the second example later). As always, the second argument, `f`, can be omitted (pass `None`) if the formula is unknown, or it can be replaced by a negated automaton if it is known.
%% Cell type:code id: tags:
``` python
spot.stutter_invariant_states(pos, f)
```
%% Output
(False, True, False, True, True, True, True, True)
%% Cell type:markdown id: tags:
For convenience, the `highligh_...()` version colors the stutter-invariant states of the automaton for display.
(That 5 is the color number for red in Spot's hard-coded palette.)
%% Cell type:code id: tags:
``` python
spot.highlight_stutter_invariant_states(pos, f, 5)
display(pos)
```
%% Output
%% Cell type:markdown id: tags:
Such a procedure gives us a map of where POR can be enabled when model checking using this automaton.
%% Cell type:markdown id: tags:
### Second example
%% Cell type:markdown id: tags:
This second example illustrates the fact that a state can be marked if it it not sutter-invariant but appear below a stutter-invariant state. We build our example automaton as the disjuction of the following two stutter-sensitive formulas, whose union is equivalent to the sutter-invariant formula `GF!a`.
%% Cell type:code id: tags:
``` python
g1 = spot.formula('GF(a & Xa) & GF!a')
g2 = spot.formula('!GF(a & Xa) & GF!a')
g = spot.formula.Or([g1, g2])
```
%% Cell type:code id: tags:
``` python
print(spot.is_stutter_invariant(g1))
print(spot.is_stutter_invariant(g2))
print(spot.is_stutter_invariant(g))
```
%% Output
False
False
True
%% Cell type:markdown id: tags:
Here are the automata for `g1` and `g2`, note that none of the states are stutter-invariant.
%% Cell type:code id: tags:
``` python
aut1 = spot.translate(g1)
aut1.set_name(str(g1))
spot.highlight_stutter_invariant_states(aut1, g1, 5)
display(aut1)
aut2 = spot.translate(g2)
aut2.set_name(str(g2))
spot.highlight_stutter_invariant_states(aut2, g2, 5)
display(aut2)
```
%% Output
%% Cell type:markdown id: tags:
Now we build the sum of these two automata. The stutter-invariance check detects that the initial state is stutter-invariant (i.e., the entire language is stutter-invariant) so all states below it are marked despite the fact that the language recognized from these individual states would not be stutter-invariant.
%% Cell type:code id: tags:
``` python
aut = spot.sum(aut1, aut2)
# At this point it is unknown if AUT is stutter-invariant
assert(aut.prop_stutter_invariant().is_maybe())
spot.highlight_stutter_invariant_states(aut, g, 5)
display(aut)
# The stutter_invariant property is set on AUT as a side effect
# of calling sutter_invariant_states() or any variant of it.
assert(aut.prop_stutter_invariant().is_true())
```
%% Output
%% Cell type:markdown id: tags:
### Third example
These procedures work regardless of the acceptance condition. Here is an example with co-Büchi acceptance.
In this case we do not even have a formula to pass as second argument, so the check will perform a complementation by determinization.
%% Cell type:code id: tags:
``` python
aut = spot.automaton('randaut --seed=30 -Q4 -A"Fin(0)" a |')
spot.highlight_stutter_invariant_states(aut, None, 5)
display(aut)
```
%% Output
%% Cell type:markdown id: tags:
If the negated automaton is already known, it can be passed as second argument (instead of the positive formula) to avoid unnecessary work.
%% Cell type:markdown id: tags:
## Sutter-invariance at the letter level
Instead of marking each state as stuttering or not, we can list the letters that we can stutter in each state.
More precisely, a state $q$ is _stutter-invariant for letter $a$_ if the membership to $L(q)$ of any word starting with $a$ is preserved by the operations that duplicate letters or remove duplicates.
$(\ell_0\ldots\ell_{i-1}\ell_i\ell_{i+1}\ldots\in L(q) \land \ell_0=a) \iff (\ell_0\ldots\ell_{i-1}\ell_i\ell_i\ell_{i+1}\ldots\in L(q)\land \ell_0=a)$
Under this definition, we can also say that $q$ is _stutter-invariant_ iff it is _stutter-invariant for any letter_.
For instance consider the following automaton, for which all words that start with $b$ are stutter invariant.
The initial state may not be declared as stutter-invariant because of words that start with $\lnot b$.
%% Cell type:code id: tags:
``` python
f = spot.formula('(!b&Xa) | Gb')
pos = spot.translate(f)
spot.highlight_stutter_invariant_states(pos, f, 5)
display(pos)
```
%% Output
%% Cell type:markdown id: tags:
The `stutter_invariant_letters()` functions returns a vector of BDDs indexed by state numbers. The BDD at index $q$ specifies all letters $\ell$ for which state $q$ would be stuttering. Note that if $q$ is stutter-invariant or reachable from a stutter-invariant state, the associated BDD will be `bddtrue` (printed as `1` below).
This interface is a bit inconveniant to use interactively, due to the fact that we need a `spot.bdd_dict` object to print a BDD.
%% Cell type:code id: tags:
``` python
sil_vec = spot.stutter_invariant_letters(pos, f)
for q in range(pos.num_states()):
print("sil_vec[{}] =".format(q), spot.bdd_format_formula(pos.get_dict(), sil_vec[q]))
```
%% Output
sil_vec[0] = 1
sil_vec[1] = 1
sil_vec[2] = 1
sil_vec[3] = b
%% Cell type:markdown id: tags:
## The set of stutter-invariant states is not always forward closed
Consider the following automaton, which is a variant of our second example above.
The language accepted from state (2) is `!GF(a & Xa) & GF!a` (this can be simplified to `FG(!a | X!a)`), while the language accepted from state (0) is `GF(a & Xa) & GF!a`. Therefore. the language accepted from state (5) is `a & X(GF!a)`. Since this is equivalent to `a & GF(!a)` state (5) recognizes stutter-invariant language, but as we can see, it is not the case that all states below (5) are also marked. In fact, states (0) can also be reached via states (7) and (6), recognizing respectively `(a & X(a & GF!a)) | (!a & X(!a & GF(a & Xa) & GF!a))` and `!a & GF(a & Xa) & GF!a))`, i.e., two stutter-sentive languages.
%% Cell type:code id: tags:
``` python
ex1 = spot.automaton("""HOA: v1
States: 8 Start: 7 AP: 1 "a" Acceptance: 2 (Inf(0)&Inf(1))
--BODY--
State: 0 [!0] 0 {1} [0] 0 [0] 1 {0}
State: 1 [0] 0 [0] 1 {0}
State: 2 [t] 2 [!0] 3 [0] 4
State: 3 [!0] 3 {0 1} [0] 4 {0 1}
State: 4 [!0] 3 {0 1}
State: 5 [0] 0 [0] 2
State: 6 [!0] 0
State: 7 [!0] 6 [0] 5
--END--""")
spot.highlight_stutter_invariant_states(ex1, None, 5)
display(ex1)