sofic 0.1.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.
- sofic/__init__.py +185 -0
- sofic/automata/__init__.py +207 -0
- sofic/automata/_config_simulation.py +40 -0
- sofic/automata/active.py +611 -0
- sofic/automata/alergia.py +222 -0
- sofic/automata/algorithms.py +376 -0
- sofic/automata/atomaton.py +58 -0
- sofic/automata/base.py +161 -0
- sofic/automata/buchi.py +23 -0
- sofic/automata/buchi_simulation.py +67 -0
- sofic/automata/canonical_dual.py +18 -0
- sofic/automata/canonical_extraction.py +122 -0
- sofic/automata/dfa.py +85 -0
- sofic/automata/dfasat.py +195 -0
- sofic/automata/edsm.py +219 -0
- sofic/automata/enumeration.py +44 -0
- sofic/automata/icdfa.py +421 -0
- sofic/automata/idfa.py +363 -0
- sofic/automata/languages/__init__.py +39 -0
- sofic/automata/languages/_quotient_utils.py +64 -0
- sofic/automata/languages/atoms.py +31 -0
- sofic/automata/languages/automaton_ops.py +243 -0
- sofic/automata/languages/base.py +67 -0
- sofic/automata/languages/operations.py +78 -0
- sofic/automata/languages/quotients.py +66 -0
- sofic/automata/languages/residuals.py +25 -0
- sofic/automata/learning.py +79 -0
- sofic/automata/nfa.py +39 -0
- sofic/automata/nwa.py +343 -0
- sofic/automata/nwa_simulation.py +56 -0
- sofic/automata/observation.py +40 -0
- sofic/automata/papni.py +301 -0
- sofic/automata/regex.py +128 -0
- sofic/automata/rfsa.py +35 -0
- sofic/automata/rpni.py +193 -0
- sofic/automata/subsequential.py +201 -0
- sofic/automata/transducer_operations.py +350 -0
- sofic/automata/transducer_simulation.py +150 -0
- sofic/automata/transducers.py +365 -0
- sofic/automata/unifilar.py +107 -0
- sofic/automata/vpa.py +1373 -0
- sofic/automata/vpa_simulation.py +53 -0
- sofic/base.py +153 -0
- sofic/core.py +47 -0
- sofic/examples/__init__.py +86 -0
- sofic/examples/epsilon_machines.py +1089 -0
- sofic/examples/processes.py +1491 -0
- sofic/examples/shifts.py +144 -0
- sofic/exceptions.py +33 -0
- sofic/generators/__init__.py +115 -0
- sofic/generators/_word_measures.py +94 -0
- sofic/generators/alternative_complexity.py +104 -0
- sofic/generators/base.py +327 -0
- sofic/generators/bidirectional_construction.py +717 -0
- sofic/generators/bidirectional_epsilon_machine.py +689 -0
- sofic/generators/block_convergence.py +668 -0
- sofic/generators/block_entropy.py +578 -0
- sofic/generators/channel_measures.py +75 -0
- sofic/generators/conversions.py +182 -0
- sofic/generators/directional_flow.py +245 -0
- sofic/generators/edge_emissions.py +36 -0
- sofic/generators/edge_machine.py +178 -0
- sofic/generators/epsilon_construction.py +193 -0
- sofic/generators/epsilon_inference.py +703 -0
- sofic/generators/epsilon_machine.py +557 -0
- sofic/generators/epsilon_transducer.py +168 -0
- sofic/generators/epsilon_transducer_construction.py +185 -0
- sofic/generators/epsilon_transducer_inference.py +499 -0
- sofic/generators/hmm_inference.py +719 -0
- sofic/generators/information_diagram.py +428 -0
- sofic/generators/lumping.py +447 -0
- sofic/generators/markov.py +100 -0
- sofic/generators/mealy.py +156 -0
- sofic/generators/measures.py +257 -0
- sofic/generators/minimal_generative_model.py +821 -0
- sofic/generators/mixed_state.py +250 -0
- sofic/generators/mixed_state_construction.py +163 -0
- sofic/generators/moore.py +75 -0
- sofic/generators/nmachine.py +78 -0
- sofic/generators/nmachine_construction.py +70 -0
- sofic/generators/pfa.py +100 -0
- sofic/generators/prob.py +291 -0
- sofic/generators/process_equivalence.py +207 -0
- sofic/generators/quasi_inference.py +74 -0
- sofic/generators/quasi_realization.py +97 -0
- sofic/generators/reversal.py +66 -0
- sofic/generators/stack_hmm.py +426 -0
- sofic/generators/stack_inference.py +509 -0
- sofic/generators/stationary.py +134 -0
- sofic/generators/stochastic.py +65 -0
- sofic/generators/synchronization.py +407 -0
- sofic/generators/topological_epsilon_enumeration.py +349 -0
- sofic/generators/words.py +226 -0
- sofic/graph.py +135 -0
- sofic/indexing.py +31 -0
- sofic/inference/__init__.py +45 -0
- sofic/inference/bayesian/__init__.py +68 -0
- sofic/inference/bayesian/comparison.py +199 -0
- sofic/inference/bayesian/counts.py +219 -0
- sofic/inference/bayesian/diversity.py +254 -0
- sofic/inference/bayesian/epsilon.py +270 -0
- sofic/inference/bayesian/hdp_hmm.py +340 -0
- sofic/inference/bayesian/markov.py +294 -0
- sofic/inference/bayesian/pymc_backend.py +71 -0
- sofic/inference/bayesian/stack_hmm.py +215 -0
- sofic/inference/model_selection.py +365 -0
- sofic/inference/spectral.py +564 -0
- sofic/operations.py +16 -0
- sofic/properties.py +339 -0
- sofic/serialization.py +450 -0
- sofic/shifts/__init__.py +48 -0
- sofic/shifts/algorithms.py +84 -0
- sofic/shifts/base.py +49 -0
- sofic/shifts/cover_construction.py +76 -0
- sofic/shifts/covers.py +47 -0
- sofic/shifts/dyck_algorithms.py +100 -0
- sofic/shifts/dyck_enumeration.py +275 -0
- sofic/shifts/markov_dyck.py +172 -0
- sofic/shifts/parry_construction.py +82 -0
- sofic/shifts/sft.py +104 -0
- sofic/shifts/sft_construction.py +52 -0
- sofic/shifts/sliding_block_code.py +156 -0
- sofic/shifts/sofic.py +111 -0
- sofic/shifts/sofic_dyck.py +110 -0
- sofic/shifts/sofic_relation.py +64 -0
- sofic/shifts/textile.py +104 -0
- sofic/shifts/tmc.py +46 -0
- sofic/shifts/tmc_construction.py +58 -0
- sofic/shifts/topological_anatomy.py +150 -0
- sofic/states.py +27 -0
- sofic/testing/__init__.py +8 -0
- sofic/testing/strategies.py +154 -0
- sofic/viz/__init__.py +16 -0
- sofic/viz/_context.py +345 -0
- sofic/viz/_edge.py +216 -0
- sofic/viz/_format.py +89 -0
- sofic/viz/_labels.py +34 -0
- sofic/viz/_names.py +17 -0
- sofic/viz/_rational.py +20 -0
- sofic/viz/_tikz_compile.py +177 -0
- sofic/viz/_tikz_format.py +122 -0
- sofic/viz/_tikz_layout.py +218 -0
- sofic/viz/assets/vaucanson.tikz +71 -0
- sofic/viz/graphviz.py +158 -0
- sofic/viz/idiagram.py +350 -0
- sofic/viz/tikz.py +381 -0
- sofic-0.1.0.dist-info/METADATA +444 -0
- sofic-0.1.0.dist-info/RECORD +150 -0
- sofic-0.1.0.dist-info/WHEEL +4 -0
- sofic-0.1.0.dist-info/licenses/LICENSE.txt +29 -0
sofic/automata/nwa.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Nested word automata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterable, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from sofic.base import StateMachine
|
|
10
|
+
from sofic.exceptions import SoficValidationError
|
|
11
|
+
from sofic.graph import (
|
|
12
|
+
ATTR_HIER_STATE,
|
|
13
|
+
ATTR_KIND,
|
|
14
|
+
ATTR_STACK_SYMBOL,
|
|
15
|
+
ATTR_SYMBOL,
|
|
16
|
+
KIND_CALL,
|
|
17
|
+
KIND_INTERNAL,
|
|
18
|
+
KIND_RETURN,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from sofic.automata.vpa import VisiblyPushdownAutomaton
|
|
23
|
+
|
|
24
|
+
_KINDS = frozenset({KIND_CALL, KIND_RETURN, KIND_INTERNAL})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class NestedWord:
|
|
29
|
+
"""Finite word together with an explicit call-return matching relation."""
|
|
30
|
+
|
|
31
|
+
symbols: tuple[Any, ...]
|
|
32
|
+
kinds: tuple[str, ...]
|
|
33
|
+
matching: tuple[int | None, ...]
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
object.__setattr__(self, "symbols", tuple(self.symbols))
|
|
37
|
+
object.__setattr__(self, "kinds", tuple(self.kinds))
|
|
38
|
+
object.__setattr__(self, "matching", tuple(self.matching))
|
|
39
|
+
self.validate()
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_visible_word(
|
|
43
|
+
cls,
|
|
44
|
+
symbols: Sequence[Any],
|
|
45
|
+
*,
|
|
46
|
+
call_alphabet: Iterable[Any],
|
|
47
|
+
return_alphabet: Iterable[Any],
|
|
48
|
+
internal_alphabet: Iterable[Any] = (),
|
|
49
|
+
) -> NestedWord:
|
|
50
|
+
"""Build the canonical nested word induced by a visible alphabet."""
|
|
51
|
+
symbols = tuple(symbols)
|
|
52
|
+
call_alphabet = frozenset(call_alphabet)
|
|
53
|
+
return_alphabet = frozenset(return_alphabet)
|
|
54
|
+
internal_alphabet = frozenset(internal_alphabet)
|
|
55
|
+
_require_disjoint_visible_alphabets(call_alphabet, return_alphabet, internal_alphabet)
|
|
56
|
+
|
|
57
|
+
kinds: list[str] = []
|
|
58
|
+
matching: list[int | None] = [None] * len(symbols)
|
|
59
|
+
stack: list[int] = []
|
|
60
|
+
for index, symbol in enumerate(symbols):
|
|
61
|
+
if symbol in call_alphabet:
|
|
62
|
+
kinds.append(KIND_CALL)
|
|
63
|
+
stack.append(index)
|
|
64
|
+
elif symbol in return_alphabet:
|
|
65
|
+
kinds.append(KIND_RETURN)
|
|
66
|
+
if stack:
|
|
67
|
+
call_index = stack.pop()
|
|
68
|
+
matching[call_index] = index
|
|
69
|
+
matching[index] = call_index
|
|
70
|
+
elif symbol in internal_alphabet:
|
|
71
|
+
kinds.append(KIND_INTERNAL)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"symbol {symbol!r} is not in the visible alphabet")
|
|
74
|
+
|
|
75
|
+
return cls(symbols=symbols, kinds=tuple(kinds), matching=tuple(matching))
|
|
76
|
+
|
|
77
|
+
def validate(self) -> None:
|
|
78
|
+
"""Raise if the matching relation is not a valid nested-word relation."""
|
|
79
|
+
if len(self.symbols) != len(self.kinds) or len(self.symbols) != len(self.matching):
|
|
80
|
+
raise SoficValidationError("symbols, kinds, and matching must have the same length")
|
|
81
|
+
|
|
82
|
+
for index, kind in enumerate(self.kinds):
|
|
83
|
+
if kind not in _KINDS:
|
|
84
|
+
raise SoficValidationError(f"invalid nested-word kind {kind!r} at position {index}")
|
|
85
|
+
|
|
86
|
+
for index, (kind, partner) in enumerate(zip(self.kinds, self.matching, strict=True)):
|
|
87
|
+
if partner is None:
|
|
88
|
+
continue
|
|
89
|
+
if not isinstance(partner, int) or partner < 0 or partner >= len(self.symbols):
|
|
90
|
+
raise SoficValidationError(f"matching partner {partner!r} out of range at position {index}")
|
|
91
|
+
if self.matching[partner] != index:
|
|
92
|
+
raise SoficValidationError("matching relation must be symmetric")
|
|
93
|
+
partner_kind = self.kinds[partner]
|
|
94
|
+
if kind == KIND_INTERNAL:
|
|
95
|
+
raise SoficValidationError("internal positions cannot be matched")
|
|
96
|
+
if kind == KIND_CALL and not (partner_kind == KIND_RETURN and index < partner):
|
|
97
|
+
raise SoficValidationError("call positions must match later return positions")
|
|
98
|
+
if kind == KIND_RETURN and not (partner_kind == KIND_CALL and partner < index):
|
|
99
|
+
raise SoficValidationError("return positions must match earlier call positions")
|
|
100
|
+
|
|
101
|
+
stack: list[int] = []
|
|
102
|
+
for index, (kind, partner) in enumerate(zip(self.kinds, self.matching, strict=True)):
|
|
103
|
+
if kind == KIND_CALL and partner is not None:
|
|
104
|
+
stack.append(index)
|
|
105
|
+
elif kind == KIND_RETURN and partner is not None:
|
|
106
|
+
if not stack or stack[-1] != partner:
|
|
107
|
+
raise SoficValidationError("matching relation must be properly nested")
|
|
108
|
+
stack.pop()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class NestedWordAutomaton(StateMachine):
|
|
112
|
+
"""Nondeterministic nested word automaton over explicit nested words."""
|
|
113
|
+
|
|
114
|
+
input_alphabet: frozenset[Any]
|
|
115
|
+
call_alphabet: frozenset[Any]
|
|
116
|
+
return_alphabet: frozenset[Any]
|
|
117
|
+
internal_alphabet: frozenset[Any]
|
|
118
|
+
hier_alphabet: frozenset[Any]
|
|
119
|
+
bottom_hier_state: Any | None
|
|
120
|
+
initial_state: Hashable | None
|
|
121
|
+
accepting_states: frozenset[Hashable]
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
input_alphabet: frozenset[Any] | None = None,
|
|
126
|
+
call_alphabet: frozenset[Any] | None = None,
|
|
127
|
+
return_alphabet: frozenset[Any] | None = None,
|
|
128
|
+
internal_alphabet: frozenset[Any] | None = None,
|
|
129
|
+
hier_alphabet: frozenset[Any] | None = None,
|
|
130
|
+
bottom_hier_state: Any | None = None,
|
|
131
|
+
initial_state: Hashable | None = None,
|
|
132
|
+
accepting_states: frozenset[Hashable] | None = None,
|
|
133
|
+
**kwargs: Any,
|
|
134
|
+
) -> None:
|
|
135
|
+
super().__init__(**kwargs)
|
|
136
|
+
self.call_alphabet = call_alphabet if call_alphabet is not None else frozenset()
|
|
137
|
+
self.return_alphabet = return_alphabet if return_alphabet is not None else frozenset()
|
|
138
|
+
self.internal_alphabet = internal_alphabet if internal_alphabet is not None else frozenset()
|
|
139
|
+
self.hier_alphabet = hier_alphabet if hier_alphabet is not None else frozenset()
|
|
140
|
+
self.bottom_hier_state = bottom_hier_state
|
|
141
|
+
self.input_alphabet = (
|
|
142
|
+
input_alphabet
|
|
143
|
+
if input_alphabet is not None
|
|
144
|
+
else self.call_alphabet | self.return_alphabet | self.internal_alphabet
|
|
145
|
+
)
|
|
146
|
+
self.initial_state = initial_state
|
|
147
|
+
self.accepting_states = accepting_states if accepting_states is not None else frozenset()
|
|
148
|
+
|
|
149
|
+
def validate(self) -> None:
|
|
150
|
+
role_alphabet = self.call_alphabet | self.return_alphabet | self.internal_alphabet
|
|
151
|
+
self._require(self.input_alphabet == role_alphabet, "input alphabet must equal the union of role alphabets")
|
|
152
|
+
if self.bottom_hier_state is not None:
|
|
153
|
+
self._require(self.bottom_hier_state in self.hier_alphabet, "bottom_hier_state must be in hier alphabet")
|
|
154
|
+
if self.initial_state is not None:
|
|
155
|
+
self._require(self.graph.has_state(self.initial_state), "missing initial state")
|
|
156
|
+
for state in self.accepting_states:
|
|
157
|
+
self._require(self.graph.has_state(state), f"missing accepting state {state!r}")
|
|
158
|
+
|
|
159
|
+
for transition in self.transitions():
|
|
160
|
+
kind = transition.data.get(ATTR_KIND)
|
|
161
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
162
|
+
self._require(kind in _KINDS, f"invalid NWA kind {kind!r}")
|
|
163
|
+
if kind == KIND_CALL:
|
|
164
|
+
self._require(symbol in self.call_alphabet, f"{symbol!r} not in call alphabet")
|
|
165
|
+
self._require(ATTR_HIER_STATE in transition.data, "call edge requires a hier_state")
|
|
166
|
+
hier_state = transition.data[ATTR_HIER_STATE]
|
|
167
|
+
self._require(hier_state in self.hier_alphabet, "call hier_state must be in hier alphabet")
|
|
168
|
+
self._require(hier_state != self.bottom_hier_state, "call edge cannot push the bottom_hier_state")
|
|
169
|
+
elif kind == KIND_RETURN:
|
|
170
|
+
self._require(symbol in self.return_alphabet, f"{symbol!r} not in return alphabet")
|
|
171
|
+
self._require(ATTR_HIER_STATE in transition.data, "return edge requires a hier_state")
|
|
172
|
+
self._require(
|
|
173
|
+
transition.data[ATTR_HIER_STATE] in self.hier_alphabet,
|
|
174
|
+
"return hier_state must be in hier alphabet",
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
self._require(symbol in self.internal_alphabet, f"{symbol!r} not in internal alphabet")
|
|
178
|
+
self._require(ATTR_HIER_STATE not in transition.data, "internal edge cannot carry a hier_state")
|
|
179
|
+
|
|
180
|
+
def add_call_transition(
|
|
181
|
+
self,
|
|
182
|
+
source: Hashable,
|
|
183
|
+
target: Hashable,
|
|
184
|
+
symbol: Any,
|
|
185
|
+
hier_state: Any,
|
|
186
|
+
**attrs: Any,
|
|
187
|
+
) -> int:
|
|
188
|
+
"""Add a call transition that stores ``hier_state`` for its matching return."""
|
|
189
|
+
data = {**attrs, ATTR_KIND: KIND_CALL, ATTR_SYMBOL: symbol, ATTR_HIER_STATE: hier_state}
|
|
190
|
+
return self.graph.add_transition(source, target, **data)
|
|
191
|
+
|
|
192
|
+
def add_return_transition(
|
|
193
|
+
self,
|
|
194
|
+
source: Hashable,
|
|
195
|
+
target: Hashable,
|
|
196
|
+
symbol: Any,
|
|
197
|
+
hier_state: Any,
|
|
198
|
+
**attrs: Any,
|
|
199
|
+
) -> int:
|
|
200
|
+
"""Add a return transition guarded by ``hier_state``."""
|
|
201
|
+
data = {**attrs, ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: symbol, ATTR_HIER_STATE: hier_state}
|
|
202
|
+
return self.graph.add_transition(source, target, **data)
|
|
203
|
+
|
|
204
|
+
def add_internal_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> int:
|
|
205
|
+
"""Add an internal transition."""
|
|
206
|
+
data = {**attrs, ATTR_KIND: KIND_INTERNAL, ATTR_SYMBOL: symbol}
|
|
207
|
+
return self.graph.add_transition(source, target, **data)
|
|
208
|
+
|
|
209
|
+
def recognizes(self, word: NestedWord) -> bool:
|
|
210
|
+
from sofic.automata.nwa_simulation import recognizes_nwa
|
|
211
|
+
|
|
212
|
+
return recognizes_nwa(self, word)
|
|
213
|
+
|
|
214
|
+
def recognizes_visible(self, symbols: Sequence[Any]) -> bool:
|
|
215
|
+
"""Recognize an ordinary word using the NWA's visible role alphabets."""
|
|
216
|
+
word = NestedWord.from_visible_word(
|
|
217
|
+
symbols,
|
|
218
|
+
call_alphabet=self.call_alphabet,
|
|
219
|
+
return_alphabet=self.return_alphabet,
|
|
220
|
+
internal_alphabet=self.internal_alphabet,
|
|
221
|
+
)
|
|
222
|
+
return self.recognizes(word)
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def from_vpa(cls, vpa: VisiblyPushdownAutomaton) -> NestedWordAutomaton:
|
|
226
|
+
"""Copy a visibly pushdown automaton into an equivalent NWA view."""
|
|
227
|
+
result = cls(
|
|
228
|
+
input_alphabet=vpa.input_alphabet,
|
|
229
|
+
call_alphabet=vpa.call_alphabet,
|
|
230
|
+
return_alphabet=vpa.return_alphabet,
|
|
231
|
+
internal_alphabet=vpa.internal_alphabet,
|
|
232
|
+
hier_alphabet=vpa.stack_alphabet,
|
|
233
|
+
bottom_hier_state=vpa.bottom_stack_symbol,
|
|
234
|
+
initial_state=vpa.initial_state,
|
|
235
|
+
accepting_states=vpa.accepting_states,
|
|
236
|
+
)
|
|
237
|
+
for state in vpa.states():
|
|
238
|
+
result.graph.add_state(state, **vpa.graph.state_attrs(state))
|
|
239
|
+
|
|
240
|
+
for transition in vpa.transitions():
|
|
241
|
+
data = transition.data
|
|
242
|
+
kind = data.get(ATTR_KIND)
|
|
243
|
+
symbol = data.get(ATTR_SYMBOL)
|
|
244
|
+
attrs = _without_transition_keys(data, ATTR_STACK_SYMBOL)
|
|
245
|
+
if kind == KIND_CALL:
|
|
246
|
+
result.add_call_transition(
|
|
247
|
+
transition.source,
|
|
248
|
+
transition.target,
|
|
249
|
+
symbol,
|
|
250
|
+
data.get(ATTR_STACK_SYMBOL),
|
|
251
|
+
**attrs,
|
|
252
|
+
)
|
|
253
|
+
elif kind == KIND_RETURN:
|
|
254
|
+
for hier_state in _return_hier_states(vpa.stack_alphabet, data.get(ATTR_STACK_SYMBOL)):
|
|
255
|
+
result.add_return_transition(transition.source, transition.target, symbol, hier_state, **attrs)
|
|
256
|
+
elif kind == KIND_INTERNAL:
|
|
257
|
+
result.add_internal_transition(transition.source, transition.target, symbol, **attrs)
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
def to_vpa(self, *, tag_symbols: bool = True) -> VisiblyPushdownAutomaton:
|
|
262
|
+
"""Encode this NWA as a visibly pushdown automaton.
|
|
263
|
+
|
|
264
|
+
If ``tag_symbols`` is true, VPA symbols are role-tagged as
|
|
265
|
+
``("call", symbol)``, ``("return", symbol)``, and
|
|
266
|
+
``("internal", symbol)``. If false, the role alphabets must be disjoint.
|
|
267
|
+
"""
|
|
268
|
+
from sofic.automata.vpa import VisiblyPushdownAutomaton
|
|
269
|
+
|
|
270
|
+
if not tag_symbols:
|
|
271
|
+
_require_disjoint_visible_alphabets(self.call_alphabet, self.return_alphabet, self.internal_alphabet)
|
|
272
|
+
|
|
273
|
+
call_alphabet = frozenset(_encoded_symbol(KIND_CALL, symbol, tag_symbols) for symbol in self.call_alphabet)
|
|
274
|
+
return_alphabet = frozenset(
|
|
275
|
+
_encoded_symbol(KIND_RETURN, symbol, tag_symbols) for symbol in self.return_alphabet
|
|
276
|
+
)
|
|
277
|
+
internal_alphabet = frozenset(
|
|
278
|
+
_encoded_symbol(KIND_INTERNAL, symbol, tag_symbols) for symbol in self.internal_alphabet
|
|
279
|
+
)
|
|
280
|
+
result = VisiblyPushdownAutomaton(
|
|
281
|
+
call_alphabet=call_alphabet,
|
|
282
|
+
return_alphabet=return_alphabet,
|
|
283
|
+
internal_alphabet=internal_alphabet,
|
|
284
|
+
stack_alphabet=self.hier_alphabet,
|
|
285
|
+
bottom_stack_symbol=self.bottom_hier_state,
|
|
286
|
+
initial_state=self.initial_state,
|
|
287
|
+
accepting_states=self.accepting_states,
|
|
288
|
+
)
|
|
289
|
+
for state in self.states():
|
|
290
|
+
result.graph.add_state(state, **self.graph.state_attrs(state))
|
|
291
|
+
|
|
292
|
+
for transition in self.transitions():
|
|
293
|
+
data = transition.data
|
|
294
|
+
kind = data.get(ATTR_KIND)
|
|
295
|
+
symbol = _encoded_symbol(kind, data.get(ATTR_SYMBOL), tag_symbols)
|
|
296
|
+
attrs = _without_transition_keys(data, ATTR_HIER_STATE)
|
|
297
|
+
if kind == KIND_CALL:
|
|
298
|
+
result.add_call_transition(
|
|
299
|
+
transition.source,
|
|
300
|
+
transition.target,
|
|
301
|
+
symbol,
|
|
302
|
+
data.get(ATTR_HIER_STATE),
|
|
303
|
+
**attrs,
|
|
304
|
+
)
|
|
305
|
+
elif kind == KIND_RETURN:
|
|
306
|
+
result.add_return_transition(
|
|
307
|
+
transition.source,
|
|
308
|
+
transition.target,
|
|
309
|
+
symbol,
|
|
310
|
+
data.get(ATTR_HIER_STATE),
|
|
311
|
+
**attrs,
|
|
312
|
+
)
|
|
313
|
+
elif kind == KIND_INTERNAL:
|
|
314
|
+
result.add_internal_transition(transition.source, transition.target, symbol, **attrs)
|
|
315
|
+
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _require_disjoint_visible_alphabets(
|
|
320
|
+
call_alphabet: frozenset[Any],
|
|
321
|
+
return_alphabet: frozenset[Any],
|
|
322
|
+
internal_alphabet: frozenset[Any],
|
|
323
|
+
) -> None:
|
|
324
|
+
visible = call_alphabet | return_alphabet | internal_alphabet
|
|
325
|
+
if len(call_alphabet) + len(return_alphabet) + len(internal_alphabet) != len(visible):
|
|
326
|
+
raise ValueError("visible alphabets must be disjoint")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _without_transition_keys(data: dict[str, Any], *extra: str) -> dict[str, Any]:
|
|
330
|
+
excluded = {ATTR_KIND, ATTR_SYMBOL, *extra}
|
|
331
|
+
return {key: value for key, value in data.items() if key not in excluded}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _return_hier_states(hier_alphabet: frozenset[Any], hier_state: Any | None) -> frozenset[Any]:
|
|
335
|
+
if hier_state is not None:
|
|
336
|
+
return frozenset({hier_state})
|
|
337
|
+
return hier_alphabet
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _encoded_symbol(kind: Any, symbol: Any, tag_symbols: bool) -> Any:
|
|
341
|
+
if tag_symbols:
|
|
342
|
+
return (kind, symbol)
|
|
343
|
+
return symbol
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Simulation for nested word automata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.automata._config_simulation import simulate_configs
|
|
9
|
+
from sofic.automata.nwa import NestedWord, NestedWordAutomaton
|
|
10
|
+
from sofic.graph import ATTR_HIER_STATE, ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN
|
|
11
|
+
|
|
12
|
+
Config = tuple[Hashable, tuple[Any, ...]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def recognizes_nwa(nwa: NestedWordAutomaton, word: NestedWord) -> bool:
|
|
16
|
+
"""Return whether ``nwa`` accepts ``word``."""
|
|
17
|
+
if nwa.initial_state is None:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
word.validate()
|
|
21
|
+
initial: set[Config] = {(nwa.initial_state, ())}
|
|
22
|
+
steps = zip(word.symbols, word.kinds, word.matching, strict=True)
|
|
23
|
+
|
|
24
|
+
def step(config: Config, item: tuple[Any, Any, int | None]) -> Iterator[Config]:
|
|
25
|
+
state, stack = config
|
|
26
|
+
symbol, kind, partner = item
|
|
27
|
+
for transition in nwa.graph.out_transitions(state):
|
|
28
|
+
data = transition.data
|
|
29
|
+
if data.get(ATTR_SYMBOL) != symbol or data.get(ATTR_KIND) != kind:
|
|
30
|
+
continue
|
|
31
|
+
if kind == KIND_CALL:
|
|
32
|
+
hier_state = data.get(ATTR_HIER_STATE)
|
|
33
|
+
next_stack = stack + (hier_state,) if partner is not None else stack
|
|
34
|
+
yield (transition.target, next_stack)
|
|
35
|
+
elif kind == KIND_RETURN:
|
|
36
|
+
yield from _advance_return(nwa, transition.target, data.get(ATTR_HIER_STATE), partner, stack)
|
|
37
|
+
elif kind == KIND_INTERNAL:
|
|
38
|
+
yield (transition.target, stack)
|
|
39
|
+
|
|
40
|
+
current = simulate_configs(initial, steps, step)
|
|
41
|
+
return any(state in nwa.accepting_states for state, _stack in current)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _advance_return(
|
|
45
|
+
nwa: NestedWordAutomaton,
|
|
46
|
+
target: Hashable,
|
|
47
|
+
hier_state: Any,
|
|
48
|
+
partner: int | None,
|
|
49
|
+
stack: tuple[Any, ...],
|
|
50
|
+
) -> Iterator[Config]:
|
|
51
|
+
if partner is None:
|
|
52
|
+
if nwa.bottom_hier_state is not None and hier_state == nwa.bottom_hier_state:
|
|
53
|
+
yield (target, stack)
|
|
54
|
+
return
|
|
55
|
+
if stack and stack[-1] == hier_state:
|
|
56
|
+
yield (target, stack[:-1])
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Angluin-style observation tables for canonical extraction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sofic.automata.atomaton import Atomaton, MaximizedPrimeAtomaton
|
|
10
|
+
from sofic.automata.dfa import DFA
|
|
11
|
+
from sofic.automata.rfsa import CanonicalRFSA
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ObservationTable:
|
|
16
|
+
"""Membership table with prefix-closed access words and suffix experiments."""
|
|
17
|
+
|
|
18
|
+
access_words: frozenset[tuple[Any, ...]] = field(default_factory=lambda: frozenset({()}))
|
|
19
|
+
experiments: frozenset[tuple[Any, ...]] = field(default_factory=lambda: frozenset({()}))
|
|
20
|
+
membership: dict[tuple[Any, ...], bool] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def to_minimal_dfa(self) -> DFA:
|
|
23
|
+
from sofic.automata.canonical_extraction import observation_to_minimal_dfa
|
|
24
|
+
|
|
25
|
+
return observation_to_minimal_dfa(self)
|
|
26
|
+
|
|
27
|
+
def to_canonical_rfsa(self) -> CanonicalRFSA:
|
|
28
|
+
from sofic.automata.canonical_extraction import observation_to_canonical_rfsa
|
|
29
|
+
|
|
30
|
+
return observation_to_canonical_rfsa(self)
|
|
31
|
+
|
|
32
|
+
def to_atomaton(self) -> Atomaton:
|
|
33
|
+
from sofic.automata.canonical_extraction import observation_to_atomaton
|
|
34
|
+
|
|
35
|
+
return observation_to_atomaton(self)
|
|
36
|
+
|
|
37
|
+
def to_maximized_prime_atomaton(self) -> MaximizedPrimeAtomaton:
|
|
38
|
+
from sofic.automata.canonical_extraction import observation_to_maximized_prime_atomaton
|
|
39
|
+
|
|
40
|
+
return observation_to_maximized_prime_atomaton(self)
|