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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Automaton constructions for regular-language algebra."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Callable, Hashable, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sofic.automata.algorithms import complete, determinize, minimize, trim
|
|
10
|
+
from sofic.automata.base import LabeledAutomaton
|
|
11
|
+
from sofic.automata.dfa import DFA
|
|
12
|
+
from sofic.automata.nfa import NFA
|
|
13
|
+
from sofic.graph import ATTR_SYMBOL, EPSILON, TransitionGraph
|
|
14
|
+
|
|
15
|
+
_LEFT = object()
|
|
16
|
+
_RIGHT = object()
|
|
17
|
+
_START = object()
|
|
18
|
+
_STAR_START = object()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_nfa(aut: LabeledAutomaton) -> NFA:
|
|
22
|
+
if isinstance(aut, NFA):
|
|
23
|
+
return trim(aut)
|
|
24
|
+
nfa = NFA(
|
|
25
|
+
input_alphabet=aut.input_alphabet,
|
|
26
|
+
initial_states=aut.initial_states,
|
|
27
|
+
accepting_states=aut.accepting_states,
|
|
28
|
+
graph=aut.graph.copy(),
|
|
29
|
+
)
|
|
30
|
+
return trim(nfa)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _to_dfa(aut: LabeledAutomaton, alphabet: frozenset[Any] | None = None) -> DFA:
|
|
34
|
+
if isinstance(aut, DFA):
|
|
35
|
+
return trim(aut)
|
|
36
|
+
return trim(determinize(_to_nfa(aut), alphabet=alphabet))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def union_nfa(left: LabeledAutomaton, right: LabeledAutomaton) -> NFA:
|
|
40
|
+
left_nfa = _to_nfa(left)
|
|
41
|
+
right_nfa = _to_nfa(right)
|
|
42
|
+
alphabet = left_nfa.input_alphabet | right_nfa.input_alphabet
|
|
43
|
+
graph = TransitionGraph()
|
|
44
|
+
start = (_START,)
|
|
45
|
+
graph.add_state(start)
|
|
46
|
+
for state in left_nfa.states():
|
|
47
|
+
graph.add_state((_LEFT, state))
|
|
48
|
+
for state in right_nfa.states():
|
|
49
|
+
graph.add_state((_RIGHT, state))
|
|
50
|
+
for initial in left_nfa.initial_states:
|
|
51
|
+
graph.add_transition(start, (_LEFT, initial), **{ATTR_SYMBOL: EPSILON})
|
|
52
|
+
for initial in right_nfa.initial_states:
|
|
53
|
+
graph.add_transition(start, (_RIGHT, initial), **{ATTR_SYMBOL: EPSILON})
|
|
54
|
+
for transition in left_nfa.transitions():
|
|
55
|
+
graph.add_transition(
|
|
56
|
+
(_LEFT, transition.source),
|
|
57
|
+
(_LEFT, transition.target),
|
|
58
|
+
**transition.data,
|
|
59
|
+
)
|
|
60
|
+
for transition in right_nfa.transitions():
|
|
61
|
+
graph.add_transition(
|
|
62
|
+
(_RIGHT, transition.source),
|
|
63
|
+
(_RIGHT, transition.target),
|
|
64
|
+
**transition.data,
|
|
65
|
+
)
|
|
66
|
+
accepting = {(_LEFT, s) for s in left_nfa.accepting_states} | {(_RIGHT, s) for s in right_nfa.accepting_states}
|
|
67
|
+
return NFA(
|
|
68
|
+
input_alphabet=alphabet,
|
|
69
|
+
initial_states=frozenset({start}),
|
|
70
|
+
accepting_states=frozenset(accepting),
|
|
71
|
+
graph=graph,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _product_dfa(
|
|
76
|
+
left_dfa: DFA,
|
|
77
|
+
right_dfa: DFA,
|
|
78
|
+
alphabet: frozenset[Any],
|
|
79
|
+
accept_pred: Callable[[Hashable, Hashable], bool],
|
|
80
|
+
*,
|
|
81
|
+
complete_inputs: bool,
|
|
82
|
+
) -> DFA:
|
|
83
|
+
"""Product-construction BFS over ``left_dfa`` x ``right_dfa``.
|
|
84
|
+
|
|
85
|
+
When ``complete_inputs`` is ``False`` (intersection semantics) transitions
|
|
86
|
+
with a missing successor in either factor are skipped, yielding a sparse
|
|
87
|
+
product. When ``True`` (difference semantics) both factors are assumed total
|
|
88
|
+
so every symbol produces a successor and traps contribute to the language.
|
|
89
|
+
``accept_pred`` decides acceptance from the two component states.
|
|
90
|
+
"""
|
|
91
|
+
graph = TransitionGraph()
|
|
92
|
+
initial = (next(iter(left_dfa.initial_states)), next(iter(right_dfa.initial_states)))
|
|
93
|
+
graph.add_state(initial)
|
|
94
|
+
queue: deque[tuple[Hashable, Hashable]] = deque([initial])
|
|
95
|
+
seen = {initial}
|
|
96
|
+
while queue:
|
|
97
|
+
pair = queue.popleft()
|
|
98
|
+
for symbol in alphabet:
|
|
99
|
+
left_next = left_dfa.delta(pair[0], symbol)
|
|
100
|
+
right_next = right_dfa.delta(pair[1], symbol)
|
|
101
|
+
if not complete_inputs and (len(left_next) != 1 or len(right_next) != 1):
|
|
102
|
+
continue
|
|
103
|
+
target = (next(iter(left_next)), next(iter(right_next)))
|
|
104
|
+
if target not in seen:
|
|
105
|
+
seen.add(target)
|
|
106
|
+
graph.add_state(target)
|
|
107
|
+
queue.append(target)
|
|
108
|
+
graph.add_transition(pair, target, **{ATTR_SYMBOL: symbol})
|
|
109
|
+
accepting = {pair for pair in seen if accept_pred(pair[0], pair[1])}
|
|
110
|
+
return DFA(
|
|
111
|
+
input_alphabet=alphabet,
|
|
112
|
+
initial_states=frozenset({initial}),
|
|
113
|
+
accepting_states=frozenset(accepting),
|
|
114
|
+
graph=graph,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def intersection_dfa(left: LabeledAutomaton, right: LabeledAutomaton) -> DFA:
|
|
119
|
+
left_dfa = _to_dfa(left)
|
|
120
|
+
right_dfa = _to_dfa(right)
|
|
121
|
+
alphabet = left_dfa.input_alphabet | right_dfa.input_alphabet
|
|
122
|
+
return _product_dfa(
|
|
123
|
+
left_dfa,
|
|
124
|
+
right_dfa,
|
|
125
|
+
alphabet,
|
|
126
|
+
lambda ls, rs: ls in left_dfa.accepting_states and rs in right_dfa.accepting_states,
|
|
127
|
+
complete_inputs=False,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def complement_dfa(dfa: LabeledAutomaton, alphabet: frozenset[Any] | None = None) -> DFA:
|
|
132
|
+
symbols = _effective_alphabet(dfa) if alphabet is None else alphabet
|
|
133
|
+
complete_dfa = complete(_to_dfa(dfa, alphabet=symbols), alphabet=symbols)
|
|
134
|
+
all_states = set(complete_dfa.states())
|
|
135
|
+
accepting = all_states - set(complete_dfa.accepting_states)
|
|
136
|
+
result = complete_dfa.copy()
|
|
137
|
+
result.accepting_states = frozenset(accepting)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def difference_dfa(
|
|
142
|
+
left: LabeledAutomaton,
|
|
143
|
+
right: LabeledAutomaton,
|
|
144
|
+
alphabet: frozenset[Any] | None = None,
|
|
145
|
+
) -> DFA:
|
|
146
|
+
"""Return a DFA recognizing ``left`` minus ``right`` over ``alphabet``."""
|
|
147
|
+
symbols = alphabet if alphabet is not None else _effective_alphabet(left) | _effective_alphabet(right)
|
|
148
|
+
left_dfa = complete(_to_dfa(left, alphabet=symbols), alphabet=symbols)
|
|
149
|
+
right_dfa = complete(_to_dfa(right, alphabet=symbols), alphabet=symbols)
|
|
150
|
+
return _product_dfa(
|
|
151
|
+
left_dfa,
|
|
152
|
+
right_dfa,
|
|
153
|
+
symbols,
|
|
154
|
+
lambda ls, rs: ls in left_dfa.accepting_states and rs not in right_dfa.accepting_states,
|
|
155
|
+
complete_inputs=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def concat_nfa(left: LabeledAutomaton, right: LabeledAutomaton) -> NFA:
|
|
160
|
+
left_nfa = _to_nfa(left)
|
|
161
|
+
right_nfa = _to_nfa(right)
|
|
162
|
+
alphabet = left_nfa.input_alphabet | right_nfa.input_alphabet
|
|
163
|
+
graph = left_nfa.graph.copy()
|
|
164
|
+
for state in right_nfa.states():
|
|
165
|
+
graph.add_state((_RIGHT, state))
|
|
166
|
+
for transition in right_nfa.transitions():
|
|
167
|
+
graph.add_transition(
|
|
168
|
+
(_RIGHT, transition.source),
|
|
169
|
+
(_RIGHT, transition.target),
|
|
170
|
+
**transition.data,
|
|
171
|
+
)
|
|
172
|
+
for accept in left_nfa.accepting_states:
|
|
173
|
+
for initial in right_nfa.initial_states:
|
|
174
|
+
graph.add_transition(accept, (_RIGHT, initial), **{ATTR_SYMBOL: EPSILON})
|
|
175
|
+
accepting = {(_RIGHT, s) for s in right_nfa.accepting_states}
|
|
176
|
+
return NFA(
|
|
177
|
+
input_alphabet=alphabet,
|
|
178
|
+
initial_states=left_nfa.initial_states,
|
|
179
|
+
accepting_states=frozenset(accepting),
|
|
180
|
+
graph=graph,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def kleene_star_nfa(aut: LabeledAutomaton) -> NFA:
|
|
185
|
+
nfa = _to_nfa(aut)
|
|
186
|
+
graph = nfa.graph.copy()
|
|
187
|
+
start = (_STAR_START,)
|
|
188
|
+
graph.add_state(start)
|
|
189
|
+
for initial in nfa.initial_states:
|
|
190
|
+
graph.add_transition(start, initial, **{ATTR_SYMBOL: EPSILON})
|
|
191
|
+
for accept in nfa.accepting_states:
|
|
192
|
+
for initial in nfa.initial_states:
|
|
193
|
+
graph.add_transition(accept, initial, **{ATTR_SYMBOL: EPSILON})
|
|
194
|
+
accepting = set(nfa.accepting_states) | {start}
|
|
195
|
+
return NFA(
|
|
196
|
+
input_alphabet=nfa.input_alphabet,
|
|
197
|
+
initial_states=frozenset({start}),
|
|
198
|
+
accepting_states=frozenset(accepting),
|
|
199
|
+
graph=graph,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def left_quotient_automaton(u: Sequence[Any], aut: LabeledAutomaton) -> NFA:
|
|
204
|
+
nfa = _to_nfa(aut)
|
|
205
|
+
current = nfa.epsilon_closure(set(nfa.initial_states))
|
|
206
|
+
for symbol in u:
|
|
207
|
+
next_states: set[Hashable] = set()
|
|
208
|
+
for state in current:
|
|
209
|
+
next_states.update(nfa.delta(state, symbol))
|
|
210
|
+
current = nfa.epsilon_closure(next_states)
|
|
211
|
+
return NFA(
|
|
212
|
+
input_alphabet=nfa.input_alphabet,
|
|
213
|
+
initial_states=frozenset(current),
|
|
214
|
+
accepting_states=nfa.accepting_states,
|
|
215
|
+
graph=nfa.graph.copy(),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def right_quotient_automaton(aut: LabeledAutomaton, u: Sequence[Any]) -> NFA:
|
|
220
|
+
reversed_u = tuple(reversed(u))
|
|
221
|
+
rev = _to_nfa(aut).reverse()
|
|
222
|
+
return left_quotient_automaton(reversed_u, rev).reverse()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def minimal_dfa_from_language(aut: LabeledAutomaton) -> DFA:
|
|
226
|
+
return minimize(_to_dfa(aut))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def state_residual_languages(dfa: DFA) -> dict[Hashable, DFA]:
|
|
230
|
+
"""Map each state to the DFA for its left-quotient (right-language) residual."""
|
|
231
|
+
residuals: dict[Hashable, DFA] = {}
|
|
232
|
+
for state in dfa.states():
|
|
233
|
+
sub = dfa.copy()
|
|
234
|
+
sub.initial_states = frozenset({state})
|
|
235
|
+
sub.accepting_states = dfa.accepting_states
|
|
236
|
+
residuals[state] = minimize(_to_dfa(sub))
|
|
237
|
+
return residuals
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _effective_alphabet(aut: LabeledAutomaton) -> frozenset[Any]:
|
|
241
|
+
from sofic.automata.algorithms import _effective_alphabet as _shared
|
|
242
|
+
|
|
243
|
+
return _shared(aut)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Regular language protocol and concrete wrappers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from sofic.automata.base import LabeledAutomaton
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class RegularLanguage(Protocol):
|
|
13
|
+
"""Membership oracle for a regular (or finitely specified) language."""
|
|
14
|
+
|
|
15
|
+
def __contains__(self, word: Sequence[Any]) -> bool: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExplicitLanguage:
|
|
19
|
+
"""Language given by explicit positive/negative word sets."""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_positive", "_negative", "_alphabet")
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
positive: set[tuple[Any, ...]] | None = None,
|
|
26
|
+
negative: set[tuple[Any, ...]] | None = None,
|
|
27
|
+
alphabet: frozenset[Any] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._positive = positive if positive is not None else set()
|
|
30
|
+
self._negative = negative if negative is not None else set()
|
|
31
|
+
self._alphabet = alphabet if alphabet is not None else frozenset()
|
|
32
|
+
|
|
33
|
+
def __contains__(self, word: Sequence[Any]) -> bool:
|
|
34
|
+
key = tuple(word)
|
|
35
|
+
if key in self._negative:
|
|
36
|
+
return False
|
|
37
|
+
return key in self._positive
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def alphabet(self) -> frozenset[Any]:
|
|
41
|
+
return self._alphabet
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AutomatonLanguage:
|
|
45
|
+
"""Language recognized by a :class:`LabeledAutomaton`."""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("_automaton",)
|
|
48
|
+
|
|
49
|
+
def __init__(self, automaton: LabeledAutomaton) -> None:
|
|
50
|
+
self._automaton = automaton
|
|
51
|
+
|
|
52
|
+
def __contains__(self, word: Sequence[Any]) -> bool:
|
|
53
|
+
return self._automaton.recognizes(word)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def automaton(self) -> LabeledAutomaton:
|
|
57
|
+
return self._automaton
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_automaton(cls, aut: LabeledAutomaton) -> AutomatonLanguage:
|
|
61
|
+
return cls(aut)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def as_language(language: RegularLanguage | LabeledAutomaton) -> RegularLanguage:
|
|
65
|
+
if isinstance(language, LabeledAutomaton):
|
|
66
|
+
return AutomatonLanguage.from_automaton(language)
|
|
67
|
+
return language
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Boolean and structural regular-language operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sofic.automata.languages.automaton_ops import (
|
|
8
|
+
complement_dfa,
|
|
9
|
+
concat_nfa,
|
|
10
|
+
difference_dfa,
|
|
11
|
+
intersection_dfa,
|
|
12
|
+
kleene_star_nfa,
|
|
13
|
+
union_nfa,
|
|
14
|
+
)
|
|
15
|
+
from sofic.automata.languages.base import AutomatonLanguage, ExplicitLanguage, RegularLanguage, as_language
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def union(left: RegularLanguage, right: RegularLanguage) -> RegularLanguage:
|
|
19
|
+
left_lang = as_language(left) # type: ignore[arg-type]
|
|
20
|
+
right_lang = as_language(right) # type: ignore[arg-type]
|
|
21
|
+
if isinstance(left_lang, AutomatonLanguage) and isinstance(right_lang, AutomatonLanguage):
|
|
22
|
+
return AutomatonLanguage(union_nfa(left_lang.automaton, right_lang.automaton))
|
|
23
|
+
raise TypeError("union requires automaton-backed languages")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def intersection(left: RegularLanguage, right: RegularLanguage) -> RegularLanguage:
|
|
27
|
+
left_lang = as_language(left) # type: ignore[arg-type]
|
|
28
|
+
right_lang = as_language(right) # type: ignore[arg-type]
|
|
29
|
+
if isinstance(left_lang, AutomatonLanguage) and isinstance(right_lang, AutomatonLanguage):
|
|
30
|
+
return AutomatonLanguage(intersection_dfa(left_lang.automaton, right_lang.automaton))
|
|
31
|
+
raise TypeError("intersection requires automaton-backed languages")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def complement(language: RegularLanguage, alphabet: frozenset[Any]) -> RegularLanguage:
|
|
35
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
36
|
+
if isinstance(lang, AutomatonLanguage):
|
|
37
|
+
return AutomatonLanguage(complement_dfa(lang.automaton, alphabet))
|
|
38
|
+
raise TypeError("complement requires automaton-backed languages")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def difference(
|
|
42
|
+
left: RegularLanguage, right: RegularLanguage, alphabet: frozenset[Any] | None = None
|
|
43
|
+
) -> RegularLanguage:
|
|
44
|
+
left_lang = as_language(left) # type: ignore[arg-type]
|
|
45
|
+
right_lang = as_language(right) # type: ignore[arg-type]
|
|
46
|
+
if isinstance(left_lang, AutomatonLanguage) and isinstance(right_lang, AutomatonLanguage):
|
|
47
|
+
return AutomatonLanguage(difference_dfa(left_lang.automaton, right_lang.automaton, alphabet=alphabet))
|
|
48
|
+
raise TypeError("difference requires automaton-backed languages")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reverse(language: RegularLanguage) -> RegularLanguage:
|
|
52
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
53
|
+
if isinstance(lang, ExplicitLanguage):
|
|
54
|
+
positive = {tuple(reversed(word)) for word in lang._positive}
|
|
55
|
+
negative = {tuple(reversed(word)) for word in lang._negative}
|
|
56
|
+
return ExplicitLanguage(positive=positive, negative=negative, alphabet=lang.alphabet)
|
|
57
|
+
if isinstance(lang, AutomatonLanguage):
|
|
58
|
+
return AutomatonLanguage.from_automaton(lang.automaton.reverse())
|
|
59
|
+
raise TypeError(f"unsupported language type {type(lang)!r}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def concat(left: RegularLanguage, right: RegularLanguage) -> RegularLanguage:
|
|
63
|
+
left_lang = as_language(left) # type: ignore[arg-type]
|
|
64
|
+
right_lang = as_language(right) # type: ignore[arg-type]
|
|
65
|
+
if isinstance(left_lang, AutomatonLanguage) and isinstance(right_lang, AutomatonLanguage):
|
|
66
|
+
return AutomatonLanguage(concat_nfa(left_lang.automaton, right_lang.automaton))
|
|
67
|
+
raise TypeError("concat requires automaton-backed languages")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def kleene_star(language: RegularLanguage) -> RegularLanguage:
|
|
71
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
72
|
+
if isinstance(lang, AutomatonLanguage):
|
|
73
|
+
return AutomatonLanguage(kleene_star_nfa(lang.automaton))
|
|
74
|
+
raise TypeError("kleene_star requires automaton-backed languages")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def product(left: RegularLanguage, right: RegularLanguage) -> RegularLanguage:
|
|
78
|
+
return intersection(left, right)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Left and right quotients of regular languages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.automata.languages._quotient_utils import (
|
|
9
|
+
_prefixes_if_suffix,
|
|
10
|
+
_residual_from_state,
|
|
11
|
+
_suffixes_if_prefix,
|
|
12
|
+
_words_up_to,
|
|
13
|
+
)
|
|
14
|
+
from sofic.automata.languages.automaton_ops import (
|
|
15
|
+
left_quotient_automaton,
|
|
16
|
+
minimal_dfa_from_language,
|
|
17
|
+
right_quotient_automaton,
|
|
18
|
+
)
|
|
19
|
+
from sofic.automata.languages.base import AutomatonLanguage, ExplicitLanguage, RegularLanguage, as_language
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def left_quotient(u: Sequence[Any], language: RegularLanguage) -> RegularLanguage:
|
|
23
|
+
"""Return u^{-1} L = {w | uw in L}."""
|
|
24
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
25
|
+
if isinstance(lang, ExplicitLanguage):
|
|
26
|
+
positive = {w for uw in lang._positive for w in _suffixes_if_prefix(uw, u)}
|
|
27
|
+
negative = {w for uw in lang._negative for w in _suffixes_if_prefix(uw, u)}
|
|
28
|
+
return ExplicitLanguage(positive=positive, negative=negative, alphabet=lang.alphabet)
|
|
29
|
+
if isinstance(lang, AutomatonLanguage):
|
|
30
|
+
return AutomatonLanguage(left_quotient_automaton(u, lang.automaton))
|
|
31
|
+
raise TypeError(f"unsupported language type {type(lang)!r}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def right_quotient(language: RegularLanguage, u: Sequence[Any]) -> RegularLanguage:
|
|
35
|
+
"""Return L u^{-1} = {w | wu in L}."""
|
|
36
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
37
|
+
if isinstance(lang, ExplicitLanguage):
|
|
38
|
+
positive = {w for wu in lang._positive for w in _prefixes_if_suffix(wu, u)}
|
|
39
|
+
negative = {w for wu in lang._negative for w in _prefixes_if_suffix(wu, u)}
|
|
40
|
+
return ExplicitLanguage(positive=positive, negative=negative, alphabet=lang.alphabet)
|
|
41
|
+
if isinstance(lang, AutomatonLanguage):
|
|
42
|
+
return AutomatonLanguage(right_quotient_automaton(lang.automaton, u))
|
|
43
|
+
raise TypeError(f"unsupported language type {type(lang)!r}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def left_quotients(language: RegularLanguage) -> frozenset[RegularLanguage]:
|
|
47
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
48
|
+
if isinstance(lang, AutomatonLanguage):
|
|
49
|
+
dfa = minimal_dfa_from_language(lang.automaton)
|
|
50
|
+
quotients: set[RegularLanguage] = set()
|
|
51
|
+
for state in dfa.states():
|
|
52
|
+
quotients.add(_residual_from_state(AutomatonLanguage(dfa), state))
|
|
53
|
+
return frozenset(quotients)
|
|
54
|
+
if isinstance(lang, ExplicitLanguage):
|
|
55
|
+
alphabet = lang.alphabet
|
|
56
|
+
quotients: set[RegularLanguage] = set()
|
|
57
|
+
for length in range(4):
|
|
58
|
+
for word in _words_up_to(length, alphabet):
|
|
59
|
+
quotients.add(left_quotient(word, lang))
|
|
60
|
+
return frozenset(quotients)
|
|
61
|
+
raise TypeError(f"unsupported language type {type(lang)!r}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def residuals(language: RegularLanguage) -> frozenset[RegularLanguage]:
|
|
65
|
+
"""Residual languages Res(L) = {u^{-1}L | u in Sigma*}."""
|
|
66
|
+
return left_quotients(language)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Prime and composed residual languages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sofic.automata.languages._quotient_utils import (
|
|
6
|
+
_alphabet_of,
|
|
7
|
+
_is_union_of_others,
|
|
8
|
+
_languages_equal,
|
|
9
|
+
)
|
|
10
|
+
from sofic.automata.languages.base import ExplicitLanguage, RegularLanguage
|
|
11
|
+
from sofic.automata.languages.quotients import left_quotients
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def prime_residuals(language: RegularLanguage) -> frozenset[RegularLanguage]:
|
|
15
|
+
all_residuals = left_quotients(language)
|
|
16
|
+
return frozenset(r for r in all_residuals if not is_composed_residual(r, all_residuals))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_composed_residual(residual: RegularLanguage, all_residuals: frozenset[RegularLanguage]) -> bool:
|
|
20
|
+
if isinstance(residual, ExplicitLanguage):
|
|
21
|
+
others = [r for r in all_residuals if r is not residual and isinstance(r, ExplicitLanguage)]
|
|
22
|
+
return _is_union_of_others(residual, others)
|
|
23
|
+
others = [r for r in all_residuals if r is not residual]
|
|
24
|
+
alphabet = _alphabet_of(residual)
|
|
25
|
+
return any(_languages_equal(residual, other, alphabet) for other in others)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""NL* active learning for maximized prime átomata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.automata.atomaton import MaximizedPrimeAtomaton
|
|
9
|
+
from sofic.automata.languages.base import RegularLanguage
|
|
10
|
+
from sofic.automata.observation import ObservationTable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def learn_maximized_prime_atomaton(
|
|
14
|
+
teacher: RegularLanguage,
|
|
15
|
+
alphabet: frozenset[Any],
|
|
16
|
+
max_rounds: int = 32,
|
|
17
|
+
) -> MaximizedPrimeAtomaton:
|
|
18
|
+
"""Simulated NL* teacher loop using membership queries."""
|
|
19
|
+
access_words: set[tuple[Any, ...]] = {()}
|
|
20
|
+
experiments: set[tuple[Any, ...]] = {()}
|
|
21
|
+
|
|
22
|
+
def membership(word: Sequence[Any]) -> bool:
|
|
23
|
+
return tuple(word) in teacher
|
|
24
|
+
|
|
25
|
+
def close_table(max_depth: int = 6) -> None:
|
|
26
|
+
changed = True
|
|
27
|
+
while changed:
|
|
28
|
+
changed = False
|
|
29
|
+
for word in list(access_words):
|
|
30
|
+
if len(word) >= max_depth:
|
|
31
|
+
continue
|
|
32
|
+
for symbol in alphabet:
|
|
33
|
+
successor = word + (symbol,)
|
|
34
|
+
if successor not in access_words:
|
|
35
|
+
access_words.add(successor)
|
|
36
|
+
changed = True
|
|
37
|
+
|
|
38
|
+
for _ in range(max_rounds):
|
|
39
|
+
close_table()
|
|
40
|
+
table = _build_table(access_words, experiments, membership)
|
|
41
|
+
hypothesis = table.to_maximized_prime_atomaton()
|
|
42
|
+
counterexample = _find_counterexample(teacher, hypothesis, alphabet)
|
|
43
|
+
if counterexample is None:
|
|
44
|
+
return hypothesis
|
|
45
|
+
for length in range(len(counterexample) + 1):
|
|
46
|
+
experiments.add(counterexample[length:])
|
|
47
|
+
|
|
48
|
+
return _build_table(access_words, experiments, membership).to_maximized_prime_atomaton()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _build_table(
|
|
52
|
+
access_words: set[tuple[Any, ...]],
|
|
53
|
+
experiments: set[tuple[Any, ...]],
|
|
54
|
+
membership,
|
|
55
|
+
) -> ObservationTable:
|
|
56
|
+
membership_map: dict[tuple[Any, ...], bool] = {}
|
|
57
|
+
for prefix in access_words:
|
|
58
|
+
for suffix in experiments:
|
|
59
|
+
membership_map[prefix + suffix] = membership(prefix + suffix)
|
|
60
|
+
return ObservationTable(
|
|
61
|
+
access_words=frozenset(access_words),
|
|
62
|
+
experiments=frozenset(experiments),
|
|
63
|
+
membership=membership_map,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _find_counterexample(
|
|
68
|
+
teacher: RegularLanguage,
|
|
69
|
+
hypothesis: MaximizedPrimeAtomaton,
|
|
70
|
+
alphabet: frozenset[Any],
|
|
71
|
+
max_len: int = 8,
|
|
72
|
+
) -> tuple[Any, ...] | None:
|
|
73
|
+
from sofic.automata.languages._quotient_utils import _words_up_to
|
|
74
|
+
|
|
75
|
+
for length in range(max_len + 1):
|
|
76
|
+
for word in _words_up_to(length, alphabet):
|
|
77
|
+
if (word in teacher) != hypothesis.recognizes(word):
|
|
78
|
+
return word
|
|
79
|
+
return None
|
sofic/automata/nfa.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Nondeterministic finite automata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from sofic.automata.base import LabeledAutomaton
|
|
9
|
+
from sofic.graph import ATTR_SYMBOL, EPSILON
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sofic.automata.algorithms import MinimizationAlgorithm
|
|
13
|
+
from sofic.automata.dfa import DFA
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NFA(LabeledAutomaton):
|
|
17
|
+
"""Nondeterministic finite automaton with first-class epsilon transitions."""
|
|
18
|
+
|
|
19
|
+
def add_transition(self, source: Hashable, target: Hashable, symbol: Any = EPSILON, **attrs: Any) -> int:
|
|
20
|
+
return self.graph.add_transition(source, target, **{ATTR_SYMBOL: symbol, **attrs})
|
|
21
|
+
|
|
22
|
+
def recognizes(self, word: Sequence[Any]) -> bool:
|
|
23
|
+
final = self._run_nfa(word)
|
|
24
|
+
return bool(final & self.accepting_states)
|
|
25
|
+
|
|
26
|
+
def determinize(self, *, alphabet: frozenset[Any] | None = None) -> DFA:
|
|
27
|
+
from sofic.automata.algorithms import determinize
|
|
28
|
+
|
|
29
|
+
return determinize(self, alphabet=alphabet)
|
|
30
|
+
|
|
31
|
+
def minimize(
|
|
32
|
+
self,
|
|
33
|
+
algorithm: MinimizationAlgorithm = "hopcroft",
|
|
34
|
+
*,
|
|
35
|
+
alphabet: frozenset[Any] | None = None,
|
|
36
|
+
) -> DFA:
|
|
37
|
+
from sofic.automata.algorithms import minimize
|
|
38
|
+
|
|
39
|
+
return minimize(self, algorithm=algorithm, alphabet=alphabet)
|