morphata 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- morphata/__init__.py +78 -0
- morphata/acceptance.py +222 -0
- morphata/automaton.py +0 -0
- morphata/examples/__init__.py +0 -0
- morphata/examples/ltl.py +255 -0
- morphata/examples/nfa.py +218 -0
- morphata/examples/strel.py +545 -0
- morphata/hoa/__init__.py +34 -0
- morphata/hoa/__main__.py +40 -0
- morphata/hoa/acc_expr.py +208 -0
- morphata/hoa/exporter.py +1 -0
- morphata/hoa/hoa.lark +60 -0
- morphata/hoa/parser.py +378 -0
- morphata/py.typed +0 -0
- morphata/spec.py +121 -0
- morphata/utils.py +0 -0
- morphata-1.0.0.dist-info/METADATA +169 -0
- morphata-1.0.0.dist-info/RECORD +19 -0
- morphata-1.0.0.dist-info/WHEEL +4 -0
morphata/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Morphata: Flexible automata representations for regular and omega-regular languages.
|
|
2
|
+
|
|
3
|
+
This package provides:
|
|
4
|
+
- Pure structural automaton interfaces (Automaton, Domain, TransitionRelation)
|
|
5
|
+
- Acceptance condition expressions (morphata.acceptance)
|
|
6
|
+
- HOA format parser (morphata.hoa.parser)
|
|
7
|
+
- Example implementations (morphata.examples)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Hashable, Mapping
|
|
13
|
+
from collections.abc import Set as AbstractSet
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from typing_extensions import override
|
|
17
|
+
|
|
18
|
+
from morphata.spec import AcceptanceCondition as AcceptanceCondition
|
|
19
|
+
from morphata.spec import (
|
|
20
|
+
AlternatingTransitions,
|
|
21
|
+
BoolExpr,
|
|
22
|
+
DeterministicTransitions,
|
|
23
|
+
NonDeterministicTransitions,
|
|
24
|
+
UniversalTransitions,
|
|
25
|
+
)
|
|
26
|
+
from morphata.spec import Automaton as Automaton
|
|
27
|
+
from morphata.spec import Domain as Domain
|
|
28
|
+
from morphata.spec import InitialState as InitialState
|
|
29
|
+
from morphata.spec import TransitionRelation as TransitionRelation
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DeterministicTransitionRelation[Q: Hashable, S: Hashable](DeterministicTransitions[Q, S]):
|
|
34
|
+
data: Mapping[Q, Mapping[S, Q]]
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
def __call__(self, state: Q, symbol: S) -> Q:
|
|
38
|
+
return self.data[state][symbol]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class NonDeterministicTransitionRelation[Q: Hashable, S: Hashable](NonDeterministicTransitions[Q, S]):
|
|
43
|
+
data: Mapping[Q, Mapping[S, AbstractSet[Q]]]
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
def __call__(self, state: Q, symbol: S) -> AbstractSet[Q]:
|
|
47
|
+
return self.data[state][symbol]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class UniversalTransitionRelation[Q: Hashable, S: Hashable](UniversalTransitions[Q, S]):
|
|
52
|
+
data: Mapping[Q, Mapping[S, AbstractSet[Q]]]
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
def __call__(self, state: Q, symbol: S) -> AbstractSet[Q]:
|
|
56
|
+
return self.data[state][symbol]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class AlternatingTransitionRelation[Q: Hashable, S: Hashable](AlternatingTransitions[Q, S]):
|
|
61
|
+
data: Mapping[Q, Mapping[S, BoolExpr[Q]]]
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
def __call__(self, state: Q, symbol: S) -> BoolExpr[Q]:
|
|
65
|
+
return self.data[state][symbol]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"Domain",
|
|
70
|
+
"InitialState",
|
|
71
|
+
"AcceptanceCondition",
|
|
72
|
+
"TransitionRelation",
|
|
73
|
+
"DeterministicTransitions",
|
|
74
|
+
"NonDeterministicTransitions",
|
|
75
|
+
"UniversalTransitions",
|
|
76
|
+
"AlternatingTransitions",
|
|
77
|
+
"Automaton",
|
|
78
|
+
]
|
morphata/acceptance.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Concrete classes for acceptance conditions
|
|
2
|
+
|
|
3
|
+
Provides abstract and concrete acceptance condition classes for omega-automata
|
|
4
|
+
and finite-word automata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typing as ty
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
from attrs import frozen
|
|
13
|
+
from typing_extensions import overload
|
|
14
|
+
|
|
15
|
+
from morphata.spec import AcceptanceCondition, State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def acc_from_name(name: ty.Literal["Finite"], arg: Iterable[State], /) -> Finite[State]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def acc_from_name(name: ty.Literal["Buchi"], arg: Iterable[State], /) -> Buchi[State]: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def acc_from_name(name: ty.Literal["co-Buchi"], arg: Iterable[State], /) -> CoBuchi[State]: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def acc_from_name(name: ty.Literal["generalized-Buchi"], /, *args: Iterable[State]) -> GeneralizedBuchi[State]: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@overload
|
|
35
|
+
def acc_from_name(name: ty.Literal["generalized-co-Buchi"], /, *args: Iterable[State]) -> GeneralizedCoBuchi[State]: ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@overload
|
|
39
|
+
def acc_from_name(name: ty.Literal["Muller"], /, *args: Iterable[State]) -> Muller[State]: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def acc_from_name(name: ty.Literal["Streett"], /, *args: AccPair[State]) -> Streett[State]: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
def acc_from_name(name: ty.Literal["Rabin"], /, *args: AccPair[State]) -> Rabin[State]: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def acc_from_name(name: str, /, *args) -> AcceptanceCondition[State]:
|
|
51
|
+
match name:
|
|
52
|
+
case "Finite":
|
|
53
|
+
assert len(args) == 1, "Finite acceptance condition requires 1 accepting set"
|
|
54
|
+
arg = args[0]
|
|
55
|
+
assert isinstance(arg, Iterable)
|
|
56
|
+
return Finite(frozenset(arg))
|
|
57
|
+
case "Buchi":
|
|
58
|
+
assert len(args) == 1, "Buchi acceptance condition requires 1 accepting set"
|
|
59
|
+
arg = args[0]
|
|
60
|
+
assert isinstance(arg, Iterable)
|
|
61
|
+
return Buchi(frozenset(arg))
|
|
62
|
+
case "generalized-Buchi":
|
|
63
|
+
assert len(args) >= 0 and all(isinstance(arg, Iterable) for arg in args), (
|
|
64
|
+
"Generalized Buchi condition needs a list of accepting sets"
|
|
65
|
+
)
|
|
66
|
+
return GeneralizedBuchi(tuple(frozenset(arg) for arg in args))
|
|
67
|
+
case "co-Buchi":
|
|
68
|
+
assert len(args) == 1, "CoBuchi acceptance condition requires 1 non-accepting set"
|
|
69
|
+
arg = args[0]
|
|
70
|
+
assert isinstance(arg, Iterable)
|
|
71
|
+
return CoBuchi(frozenset(arg))
|
|
72
|
+
case "generalized-co-Buchi":
|
|
73
|
+
assert len(args) >= 0 and all(isinstance(arg, Iterable) for arg in args), (
|
|
74
|
+
"Generalized CoBuchi condition needs a list of non-accepting sets"
|
|
75
|
+
)
|
|
76
|
+
return GeneralizedCoBuchi(tuple(frozenset(arg) for arg in args))
|
|
77
|
+
case "Streett":
|
|
78
|
+
assert len(args) >= 0 and all(isinstance(arg, tuple) and len(arg) == 2 for arg in args), (
|
|
79
|
+
"Streett condition needs a list of 2-tuples of rejecting (Fin) and accepting (Inf) sets"
|
|
80
|
+
)
|
|
81
|
+
return Streett(tuple(AccPair(*(frozenset(s) for s in arg)) for arg in args))
|
|
82
|
+
case "Rabin":
|
|
83
|
+
assert len(args) >= 0 and all(isinstance(arg, tuple) and len(arg) == 2 for arg in args), (
|
|
84
|
+
"Rabin condition needs a list of 2-tuples of rejecting (Fin) and accepting (Inf) sets"
|
|
85
|
+
)
|
|
86
|
+
return Rabin(tuple(AccPair(*(frozenset(s) for s in arg)) for arg in args))
|
|
87
|
+
|
|
88
|
+
case _:
|
|
89
|
+
raise ValueError(f"Unknown/unsupported named acceptance condition: {name} {args=}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# @frozen
|
|
93
|
+
# class GenericCondition(AcceptanceCondition[State]):
|
|
94
|
+
# acceptance_sets: tuple[frozenset[State], ...]
|
|
95
|
+
# expr: AccExpr
|
|
96
|
+
|
|
97
|
+
# @override
|
|
98
|
+
# def __len__(self) -> int:
|
|
99
|
+
# return self.num_sets
|
|
100
|
+
|
|
101
|
+
# @override
|
|
102
|
+
# def to_expr(self) -> AccExpr:
|
|
103
|
+
# return self.expr
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@frozen
|
|
107
|
+
class Finite(AcceptanceCondition[State]):
|
|
108
|
+
"""Finite-word acceptance condition.
|
|
109
|
+
|
|
110
|
+
For finite automata, acceptance means reaching a final state. This uses
|
|
111
|
+
a single acceptance set to mark final states.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
accepting: frozenset[State]
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def is_omega_regular(cls) -> bool:
|
|
118
|
+
"""Finite-word acceptance is not omega-regular."""
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@frozen
|
|
123
|
+
class Buchi(AcceptanceCondition[State]):
|
|
124
|
+
"""Büchi condition: a run, r, is accepting iff inf(r) intersects with `accepting`"""
|
|
125
|
+
|
|
126
|
+
accepting: frozenset[State]
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def is_omega_regular(cls) -> bool:
|
|
130
|
+
"""Büchi acceptance is omega-regular."""
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@frozen
|
|
135
|
+
class GeneralizedBuchi(AcceptanceCondition[State]):
|
|
136
|
+
"""Generalized Büchi condition: a run, r, is accepting iff inf(r) intersects with `accepting[i]` for some i"""
|
|
137
|
+
|
|
138
|
+
accepting: tuple[frozenset[State], ...]
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def is_omega_regular(cls) -> bool:
|
|
142
|
+
"""Generalized Büchi acceptance is omega-regular."""
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@frozen
|
|
147
|
+
class CoBuchi(AcceptanceCondition[State]):
|
|
148
|
+
"""co-Büchi condition: a run, r, is accepting iff inf(r) does not intersect with `rejecting`"""
|
|
149
|
+
|
|
150
|
+
rejecting: frozenset[State]
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def is_omega_regular(cls) -> bool:
|
|
154
|
+
"""co-Büchi acceptance is omega-regular."""
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@frozen
|
|
159
|
+
class GeneralizedCoBuchi(AcceptanceCondition[State]):
|
|
160
|
+
"""Generalized co-Büchi condition: a run, r, is accepting iff inf(r) does not intersect with `rejecting[i]` for some i"""
|
|
161
|
+
|
|
162
|
+
rejecting: tuple[frozenset[State], ...]
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def is_omega_regular(cls) -> bool:
|
|
166
|
+
"""Generalized co-Büchi acceptance is omega-regular."""
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class AccPair(ty.NamedTuple, ty.Generic[State]):
|
|
171
|
+
"""Pair of accepting and rejecting state sets for Rabin/Streett conditions."""
|
|
172
|
+
|
|
173
|
+
rejecting: frozenset[State]
|
|
174
|
+
"""States that must not appear infinitely often"""
|
|
175
|
+
accepting: frozenset[State]
|
|
176
|
+
"""States that must appear infinitely often"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@frozen
|
|
180
|
+
class Streett(AcceptanceCondition[State]):
|
|
181
|
+
"""Streett condition: a run, r, is accpting iff _for all_ `i`, we have that inf(r) does not intersect with `pairs[i].rejecting` and does intersect with `pairs[i].accepting`"""
|
|
182
|
+
|
|
183
|
+
pairs: tuple[AccPair[State], ...]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def index(self) -> int:
|
|
187
|
+
"""Number of pairs in this condition."""
|
|
188
|
+
return len(self.pairs)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def is_omega_regular(cls) -> bool:
|
|
192
|
+
"""Streett acceptance is omega-regular."""
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@frozen
|
|
197
|
+
class Rabin(AcceptanceCondition[State]):
|
|
198
|
+
"""Rabin condition: a run, r, is accpting iff _for some_ `i`, we have that inf(r) does not intersect with `pairs[i].rejecting` and does intersect with `pairs[i].accepting`"""
|
|
199
|
+
|
|
200
|
+
pairs: tuple[AccPair[State], ...]
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def index(self) -> int:
|
|
204
|
+
"""Number of pairs in this condition."""
|
|
205
|
+
return len(self.pairs)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def is_omega_regular(cls) -> bool:
|
|
209
|
+
"""Rabin acceptance is omega-regular."""
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@frozen
|
|
214
|
+
class Muller(AcceptanceCondition[State]):
|
|
215
|
+
"""Muller condition: a run, r, is accepting iff for some `i`, we have that inf(r) is exactly `sets[i]`"""
|
|
216
|
+
|
|
217
|
+
sets: tuple[frozenset[State], ...]
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def is_omega_regular(cls) -> bool:
|
|
221
|
+
"""Muller acceptance is omega-regular."""
|
|
222
|
+
return True
|
morphata/automaton.py
ADDED
|
File without changes
|
|
File without changes
|
morphata/examples/ltl.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Implement a simple LTL to alternating automaton procedure.
|
|
2
|
+
|
|
3
|
+
The translation supports
|
|
4
|
+
- LTL to Alternating Büchi Word Automata
|
|
5
|
+
- LTLf to Alternating Finite Automata
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import functools
|
|
12
|
+
import typing as ty
|
|
13
|
+
from collections import deque
|
|
14
|
+
from collections.abc import Collection, Hashable, Iterable, Iterator, Mapping
|
|
15
|
+
from collections.abc import Set as AbstractSet
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
import logic_asts as logic
|
|
19
|
+
import logic_asts.ltl as ltl
|
|
20
|
+
from typing_extensions import override
|
|
21
|
+
|
|
22
|
+
import morphata
|
|
23
|
+
from morphata.acceptance import Buchi, Finite
|
|
24
|
+
|
|
25
|
+
AP = ty.TypeVar("AP", bound=Hashable)
|
|
26
|
+
type Input[AP] = AbstractSet[AP]
|
|
27
|
+
type BoolExpr[Sym: Hashable] = logic.base.BaseExpr[Sym]
|
|
28
|
+
type LTLExpr[AP] = ltl.LTLExpr[AP]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ltl_to_automaton(formula: LTLExpr[AP], *, finite: bool = False) -> morphata.Automaton[int, Input[AP]]:
|
|
32
|
+
"""Convert an LTL expression into an alternating automaton."""
|
|
33
|
+
|
|
34
|
+
formula = ty.cast(LTLExpr[AP], formula.expand())
|
|
35
|
+
atomic_predicates: frozenset[AP] = frozenset(
|
|
36
|
+
ty.cast(AP, e.name) for e in formula.iter_subtree() if isinstance(e, ltl.Variable)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
transitions: dict[int, Mapping[Input[AP], BoolExpr[int]]] = dict()
|
|
40
|
+
initial_node = formula
|
|
41
|
+
final_states: set[int] = set()
|
|
42
|
+
|
|
43
|
+
# Now, we want to remap to integer nodes such that:
|
|
44
|
+
# 1. 0 is the initial node
|
|
45
|
+
# 2. the rest of the nodes are contiguous integers
|
|
46
|
+
# 3. Unreachable nodes not there
|
|
47
|
+
mappings: dict[logic.Variable[LTLExpr[AP]] | logic.Not, int] = dict()
|
|
48
|
+
|
|
49
|
+
expr_remap_cache: dict[BoolExpr[LTLExpr[AP]], BoolExpr[int]] = dict()
|
|
50
|
+
|
|
51
|
+
def _remap_bool_expr(node: BoolExpr[LTLExpr[AP]]) -> BoolExpr[int]:
|
|
52
|
+
if node in expr_remap_cache:
|
|
53
|
+
return expr_remap_cache[node]
|
|
54
|
+
match node:
|
|
55
|
+
case logic.Literal():
|
|
56
|
+
return node
|
|
57
|
+
case logic.Variable():
|
|
58
|
+
return logic.Variable(mappings.setdefault(node, len(mappings)))
|
|
59
|
+
case logic.Not(arg):
|
|
60
|
+
# Assumes that the input node was coverted to NNF
|
|
61
|
+
assert isinstance(arg, logic.Variable)
|
|
62
|
+
# Make a new variable for Not(var)
|
|
63
|
+
return logic.Variable(mappings.setdefault(node, len(mappings)))
|
|
64
|
+
case logic.And(args) | logic.Or(args) as nary_node:
|
|
65
|
+
return dataclasses.replace(
|
|
66
|
+
nary_node, args=tuple(_remap_bool_expr(ty.cast(BoolExpr[LTLExpr[AP]], arg)) for arg in args)
|
|
67
|
+
)
|
|
68
|
+
case _:
|
|
69
|
+
raise RuntimeError("unreachable")
|
|
70
|
+
|
|
71
|
+
# Queue containing list of reachable nodes that need to be remapped.
|
|
72
|
+
# The order of insertion in the queue is the reachability order
|
|
73
|
+
queue: deque[logic.Variable[LTLExpr[AP]] | logic.Not] = deque()
|
|
74
|
+
queue.append(logic.Variable(formula))
|
|
75
|
+
# We will build the remap dict first
|
|
76
|
+
while len(queue) > 0:
|
|
77
|
+
# Pop a node_id
|
|
78
|
+
node = queue.popleft()
|
|
79
|
+
# Remap it
|
|
80
|
+
node_id = mappings.setdefault(node, len(mappings))
|
|
81
|
+
if node_id in transitions:
|
|
82
|
+
continue
|
|
83
|
+
# Check if the node is accepting/final or not.
|
|
84
|
+
if isinstance(node, logic.Variable):
|
|
85
|
+
node_is_final = _is_accepting_node_expr(node.name)
|
|
86
|
+
else:
|
|
87
|
+
assert isinstance(node, logic.Not) and isinstance(node.arg, logic.Variable)
|
|
88
|
+
node_is_final = _is_accepting_node_expr(ty.cast(LTLExpr[AP], node.arg.name))
|
|
89
|
+
if node_is_final:
|
|
90
|
+
final_states.add(node_id)
|
|
91
|
+
# Add the reachable nodes in the next step (Variables/Not(Variable)) if they are not already mapped
|
|
92
|
+
reachable: dict[Input[AP], BoolExpr[int]] = dict()
|
|
93
|
+
sym: Input[AP]
|
|
94
|
+
for sym in _powerset(atomic_predicates):
|
|
95
|
+
# Compute the possible successor polynomial
|
|
96
|
+
successor = _aut_expansion_rule(node, sym) # pyrefly: ignore[bad-argument-type]
|
|
97
|
+
# We want to add the NNF leaf nodes to the queue, i.e., Not(var) is a leaf node, iff they are not already visited
|
|
98
|
+
queue.extend((e for e in successor.atomic_predicates(assume_nnf=True) if e not in mappings))
|
|
99
|
+
# pyrefly: ignore[bad-argument-type]
|
|
100
|
+
reachable[sym] = _remap_bool_expr(successor)
|
|
101
|
+
transitions[node_id] = reachable
|
|
102
|
+
|
|
103
|
+
assert mappings[logic.Variable(initial_node)] == 0
|
|
104
|
+
assert 0 in transitions
|
|
105
|
+
|
|
106
|
+
domain = LTLDomain(
|
|
107
|
+
frozenset(transitions.keys()),
|
|
108
|
+
atomic_predicates,
|
|
109
|
+
)
|
|
110
|
+
transition_fn = morphata.AlternatingTransitionRelation(transitions)
|
|
111
|
+
acceptance: Buchi[int] | Finite[int]
|
|
112
|
+
if finite:
|
|
113
|
+
acceptance = Finite(frozenset(final_states))
|
|
114
|
+
else:
|
|
115
|
+
acceptance = Buchi(frozenset(final_states))
|
|
116
|
+
return morphata.Automaton(
|
|
117
|
+
domain=domain, initial=mappings[logic.Variable(initial_node)], delta=transition_fn, acceptance=acceptance
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _is_accepting_node_expr(expr: LTLExpr[AP]) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Nodes that are of the form ~(a U b) are accepting, along with the literal True obviously.
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(expr, ltl.Literal):
|
|
126
|
+
return expr.value
|
|
127
|
+
|
|
128
|
+
if isinstance(expr, ltl.Not) and isinstance(expr.arg, ltl.Until):
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
# G a = ~ F ~a = ~(T U ~a)
|
|
132
|
+
if isinstance(expr, ltl.Always):
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _aut_expansion_rule(expr: LTLExpr[AP], symbol: Input[AP]) -> BoolExpr[LTLExpr[AP]]:
|
|
138
|
+
"""Symbolic expansion of the automaton state corresponding to a given expression"""
|
|
139
|
+
|
|
140
|
+
def _recurse(_state: LTLExpr[AP]) -> BoolExpr[LTLExpr[AP]]:
|
|
141
|
+
return _aut_expansion_rule(_state, symbol)
|
|
142
|
+
|
|
143
|
+
def _as_poly(arg: logic.Expr) -> BoolExpr[LTLExpr[AP]]:
|
|
144
|
+
return ty.cast(BoolExpr[LTLExpr[AP]], arg)
|
|
145
|
+
|
|
146
|
+
def var(arg: LTLExpr[AP]) -> logic.Variable[LTLExpr[AP]]:
|
|
147
|
+
return logic.Variable(arg)
|
|
148
|
+
|
|
149
|
+
match expr:
|
|
150
|
+
case ltl.Literal(value):
|
|
151
|
+
return logic.Literal(value)
|
|
152
|
+
case ltl.Variable(name):
|
|
153
|
+
return logic.Literal(name in symbol)
|
|
154
|
+
case ltl.Not(arg):
|
|
155
|
+
return logic.Not(_recurse(_as_ltl(arg)))
|
|
156
|
+
case ltl.And(args):
|
|
157
|
+
return functools.reduce(lambda a, b: a & b, map(lambda a: _recurse(_as_ltl(a)), args))
|
|
158
|
+
case ltl.Or(args):
|
|
159
|
+
return functools.reduce(lambda a, b: a | b, map(lambda a: _recurse(_as_ltl(a)), args))
|
|
160
|
+
case ltl.Next(arg, steps):
|
|
161
|
+
assert steps is None or steps == 1, (
|
|
162
|
+
"should expand all intervals using `expand` before using automaton expansion rules"
|
|
163
|
+
)
|
|
164
|
+
return var(_as_ltl(arg))
|
|
165
|
+
case ltl.Always(arg, interval):
|
|
166
|
+
assert interval is None or interval.is_untimed(), (
|
|
167
|
+
"should expand all intervals using `expand` before using automaton expansion rules"
|
|
168
|
+
)
|
|
169
|
+
# Expand untimed always to: arg & X G arg -> arg & X expr
|
|
170
|
+
return _as_poly(_recurse(_as_ltl(arg)) & var(expr))
|
|
171
|
+
case ltl.Eventually(arg, interval):
|
|
172
|
+
assert interval is None or interval.is_untimed(), (
|
|
173
|
+
"should expand all intervals using `expand` before using automaton expansion rules"
|
|
174
|
+
)
|
|
175
|
+
# Expand untimed always to: arg | X F arg
|
|
176
|
+
return _as_poly(_recurse(_as_ltl(arg)) | var(expr))
|
|
177
|
+
case ltl.Until(lhs, rhs, interval):
|
|
178
|
+
assert interval is None or interval.is_untimed(), (
|
|
179
|
+
"should expand all intervals using `expand` before using automaton expansion rules"
|
|
180
|
+
)
|
|
181
|
+
# Expand to: rhs | (lhs & X expr )
|
|
182
|
+
lhs = _as_ltl(lhs)
|
|
183
|
+
rhs = _as_ltl(rhs)
|
|
184
|
+
return _as_poly(_recurse(rhs) | (_recurse(lhs) & var(expr)))
|
|
185
|
+
case _:
|
|
186
|
+
raise TypeError(f"Unknown expression type {type(expr)}")
|
|
187
|
+
raise TypeError(f"Unknown expression type {type(expr)}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _powerset[T](elements: Collection[T]) -> Iterable[AbstractSet[T]]:
|
|
191
|
+
from itertools import chain, combinations
|
|
192
|
+
|
|
193
|
+
return chain.from_iterable(map(frozenset, combinations(elements, r)) for r in range(len(elements) + 1))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class LTLDomain(morphata.Domain[int, Input[AP]]):
|
|
198
|
+
_states: AbstractSet[int]
|
|
199
|
+
atomic_predicates: AbstractSet[AP]
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
@override
|
|
203
|
+
def states(self) -> Iterable[int] | None:
|
|
204
|
+
yield from self._states
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
@override
|
|
208
|
+
def symbols(self) -> Iterable[Input[AP]] | None:
|
|
209
|
+
"""Return a powerset of the atomic predicats"""
|
|
210
|
+
yield from _powerset(self.atomic_predicates)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _iter_expr_nnf[Var: Hashable](expr: BoolExpr[Var]) -> Iterator[BoolExpr[Var]]:
|
|
214
|
+
"""Iterate over a propositional logic expression after converting it to NNF form. If
|
|
215
|
+
there is a `Not(var)` in the expression, it will not yield `var`.
|
|
216
|
+
|
|
217
|
+
Otherwise, it has the same guarantees as `Expr.iter_subtree`
|
|
218
|
+
"""
|
|
219
|
+
expr_nnf = expr.to_nnf()
|
|
220
|
+
assert logic.is_propositional_logic(expr_nnf)
|
|
221
|
+
stack: deque[BoolExpr[Var]] = deque([expr_nnf])
|
|
222
|
+
visited: set[BoolExpr[Var]] = set()
|
|
223
|
+
|
|
224
|
+
while stack:
|
|
225
|
+
subexpr = stack[-1]
|
|
226
|
+
if isinstance(subexpr, logic.Not):
|
|
227
|
+
# Don't put in the arguments of NOT
|
|
228
|
+
assert isinstance(subexpr.arg, logic.Variable), "Expected to receive NNF expression"
|
|
229
|
+
need_to_visit_children = set()
|
|
230
|
+
else:
|
|
231
|
+
need_to_visit_children = {
|
|
232
|
+
ty.cast(BoolExpr[Var], child)
|
|
233
|
+
for child in subexpr.children() # We need to visit `child`
|
|
234
|
+
if child not in visited # if it hasn't already been visited
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if visited.issuperset(need_to_visit_children):
|
|
238
|
+
# subexpr is a leaf (the set is empty) or it's children have been
|
|
239
|
+
# yielded get rid of it from the stack
|
|
240
|
+
stack.pop()
|
|
241
|
+
# Add subexpr to visited
|
|
242
|
+
visited.add(subexpr)
|
|
243
|
+
# post-order return it
|
|
244
|
+
yield subexpr
|
|
245
|
+
else:
|
|
246
|
+
# mid-level node
|
|
247
|
+
# Add relevant children to stack
|
|
248
|
+
stack.extend(need_to_visit_children)
|
|
249
|
+
# Yield the remaining nodes in the stack in reverse order
|
|
250
|
+
yield from reversed(stack)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _as_ltl[AP: Hashable](expr: logic.Expr) -> LTLExpr[AP]:
|
|
254
|
+
"""Cast logical expression to LTL expression."""
|
|
255
|
+
return ty.cast(LTLExpr[AP], expr)
|