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,365 @@
|
|
|
1
|
+
"""Finite-state transducers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Hashable, Mapping, Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from sofic.base import StateMachine
|
|
13
|
+
from sofic.exceptions import StochasticValidationError
|
|
14
|
+
from sofic.graph import ATTR_EMISSION, ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL, EPSILON
|
|
15
|
+
|
|
16
|
+
ERROR_SYMBOL = "?"
|
|
17
|
+
ERROR_STATE = "?"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Transducer(StateMachine):
|
|
21
|
+
"""Non-probabilistic input-to-output machine."""
|
|
22
|
+
|
|
23
|
+
input_alphabet: frozenset[Any]
|
|
24
|
+
output_alphabet: frozenset[Any]
|
|
25
|
+
initial_states: frozenset[Hashable]
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
input_alphabet: frozenset[Any] | None = None,
|
|
30
|
+
output_alphabet: frozenset[Any] | None = None,
|
|
31
|
+
initial_states: frozenset[Hashable] | None = None,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(**kwargs)
|
|
35
|
+
self.input_alphabet = input_alphabet if input_alphabet is not None else frozenset()
|
|
36
|
+
self.output_alphabet = output_alphabet if output_alphabet is not None else frozenset()
|
|
37
|
+
self.initial_states = initial_states if initial_states is not None else frozenset()
|
|
38
|
+
|
|
39
|
+
def validate(self) -> None:
|
|
40
|
+
for state in self.initial_states:
|
|
41
|
+
self._require(self.graph.has_state(state), f"missing initial state {state!r}")
|
|
42
|
+
|
|
43
|
+
def is_deterministic(self) -> bool:
|
|
44
|
+
"""Return whether each state has at most one transition per input symbol."""
|
|
45
|
+
from sofic.properties import is_deterministic_transducer
|
|
46
|
+
|
|
47
|
+
return is_deterministic_transducer(self)
|
|
48
|
+
|
|
49
|
+
def alphabets(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
node: bool = False,
|
|
53
|
+
) -> tuple[frozenset[Any], frozenset[Any]] | tuple[dict[Hashable, frozenset[Any]], dict[Hashable, frozenset[Any]]]:
|
|
54
|
+
"""Return effective input and output alphabets.
|
|
55
|
+
|
|
56
|
+
``EPSILON`` transitions are operationally meaningful but are not part
|
|
57
|
+
of the external alphabets returned here.
|
|
58
|
+
"""
|
|
59
|
+
input_by_state: dict[Hashable, set[Any]] = {state: set() for state in self.states()}
|
|
60
|
+
output_by_state: dict[Hashable, set[Any]] = {state: set() for state in self.states()}
|
|
61
|
+
inputs: set[Any] = set(self.input_alphabet)
|
|
62
|
+
outputs: set[Any] = set(self.output_alphabet)
|
|
63
|
+
for transition in self.transitions():
|
|
64
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
65
|
+
if symbol is not None and symbol is not EPSILON:
|
|
66
|
+
inputs.add(symbol)
|
|
67
|
+
input_by_state.setdefault(transition.source, set()).add(symbol)
|
|
68
|
+
output = transition.data.get(ATTR_OUTPUT)
|
|
69
|
+
if output is not None and output is not EPSILON:
|
|
70
|
+
outputs.add(output)
|
|
71
|
+
output_by_state.setdefault(transition.source, set()).add(output)
|
|
72
|
+
if node:
|
|
73
|
+
return (
|
|
74
|
+
{state: frozenset(symbols) for state, symbols in input_by_state.items()},
|
|
75
|
+
{state: frozenset(symbols) for state, symbols in output_by_state.items()},
|
|
76
|
+
)
|
|
77
|
+
return frozenset(inputs), frozenset(outputs)
|
|
78
|
+
|
|
79
|
+
def is_complete(self, alphabet: frozenset[Any] | None = None) -> bool:
|
|
80
|
+
"""Return whether every state has an outgoing edge for each input symbol."""
|
|
81
|
+
inputs = alphabet if alphabet is not None else self.alphabets()[0]
|
|
82
|
+
input_by_state, _ = self.alphabets(node=True)
|
|
83
|
+
return all(inputs <= symbols for symbols in input_by_state.values())
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def transduce(self, word: Sequence[Any]) -> set[tuple[Any, ...]]:
|
|
87
|
+
"""Return possible output sequences for ``word``."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MealyMachine(Transducer):
|
|
91
|
+
"""Output symbols on transitions."""
|
|
92
|
+
|
|
93
|
+
def add_transition(
|
|
94
|
+
self,
|
|
95
|
+
source: Hashable,
|
|
96
|
+
target: Hashable,
|
|
97
|
+
symbol: Any,
|
|
98
|
+
output: Any | None = None,
|
|
99
|
+
*,
|
|
100
|
+
prob: float | None = None,
|
|
101
|
+
**attrs: Any,
|
|
102
|
+
) -> int:
|
|
103
|
+
"""Add a transition with input ``symbol`` and optional output.
|
|
104
|
+
|
|
105
|
+
Use :data:`sofic.graph.EPSILON` for empty input or empty output.
|
|
106
|
+
Omitting ``prob`` keeps the transition topological; probability-aware
|
|
107
|
+
helpers treat missing probabilities as weight ``1``.
|
|
108
|
+
"""
|
|
109
|
+
data = {ATTR_SYMBOL: symbol, **attrs}
|
|
110
|
+
if output is not None:
|
|
111
|
+
data[ATTR_OUTPUT] = output
|
|
112
|
+
if prob is not None:
|
|
113
|
+
data[ATTR_PROB] = float(prob)
|
|
114
|
+
return self.graph.add_transition(source, target, **data)
|
|
115
|
+
|
|
116
|
+
def transduce(self, word: Sequence[Any]) -> set[tuple[Any, ...]]:
|
|
117
|
+
from sofic.automata.transducer_simulation import transduce_mealy
|
|
118
|
+
|
|
119
|
+
return transduce_mealy(self, word)
|
|
120
|
+
|
|
121
|
+
def complete(
|
|
122
|
+
self,
|
|
123
|
+
alphabet: frozenset[Any] | None = None,
|
|
124
|
+
*,
|
|
125
|
+
reject: Hashable = ERROR_STATE,
|
|
126
|
+
error_output: Any = ERROR_SYMBOL,
|
|
127
|
+
copy: bool = True,
|
|
128
|
+
) -> MealyMachine:
|
|
129
|
+
"""Return a complete transducer by adding reject/error transitions.
|
|
130
|
+
|
|
131
|
+
If every state already has an outgoing edge for every symbol in
|
|
132
|
+
``alphabet``, the reject sink is not added — an unused absorbing error
|
|
133
|
+
component would make the driven joint process reducible with a
|
|
134
|
+
non-unique stationary law.
|
|
135
|
+
"""
|
|
136
|
+
result = self.copy() if copy else self
|
|
137
|
+
symbols = alphabet if alphabet is not None else result.alphabets()[0]
|
|
138
|
+
if not symbols:
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
gaps: list[tuple[Hashable, Any]] = []
|
|
142
|
+
for state in list(result.states()):
|
|
143
|
+
outgoing = {
|
|
144
|
+
transition.data.get(ATTR_SYMBOL)
|
|
145
|
+
for transition in result.graph.out_transitions(state)
|
|
146
|
+
if transition.data.get(ATTR_SYMBOL) is not EPSILON
|
|
147
|
+
}
|
|
148
|
+
for symbol in symbols - outgoing:
|
|
149
|
+
gaps.append((state, symbol))
|
|
150
|
+
|
|
151
|
+
result.input_alphabet = result.input_alphabet | frozenset(symbols)
|
|
152
|
+
if not gaps:
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
result.graph.add_state(reject)
|
|
156
|
+
for state, symbol in gaps:
|
|
157
|
+
result.add_transition(state, reject, symbol, error_output, prob=1.0)
|
|
158
|
+
|
|
159
|
+
for symbol in symbols:
|
|
160
|
+
if not any(
|
|
161
|
+
transition.data.get(ATTR_SYMBOL) == symbol for transition in result.graph.out_transitions(reject)
|
|
162
|
+
):
|
|
163
|
+
result.add_transition(reject, reject, symbol, error_output, prob=1.0)
|
|
164
|
+
|
|
165
|
+
result.output_alphabet = result.output_alphabet | frozenset({error_output})
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
def input_machine(self, *, build: bool = False) -> Any:
|
|
169
|
+
"""Return a Mealy HMM for the topological input language."""
|
|
170
|
+
return _partial_machine(self, ATTR_SYMBOL, build=build)
|
|
171
|
+
|
|
172
|
+
def output_machine(self, *, build: bool = False) -> Any:
|
|
173
|
+
"""Return a Mealy HMM for the topological output language."""
|
|
174
|
+
return _partial_machine(self, ATTR_OUTPUT, build=build)
|
|
175
|
+
|
|
176
|
+
def compose(self, other: MealyMachine, **kwargs: Any) -> MealyMachine:
|
|
177
|
+
"""Return the serial composition ``other`` after this transducer."""
|
|
178
|
+
from sofic.automata.transducer_operations import compose_tt
|
|
179
|
+
|
|
180
|
+
return compose_tt((self, other), **kwargs)
|
|
181
|
+
|
|
182
|
+
def joint_machine(self, generator: Any, **kwargs: Any) -> Any:
|
|
183
|
+
"""Return the joint input/output generator induced by ``generator``."""
|
|
184
|
+
from sofic.automata.transducer_operations import compose_tg
|
|
185
|
+
|
|
186
|
+
return compose_tg(self, generator, joint=True, **kwargs)
|
|
187
|
+
|
|
188
|
+
def transduce_generator(self, generator: Any, **kwargs: Any) -> Any:
|
|
189
|
+
"""Return the output generator induced by driving this transducer."""
|
|
190
|
+
from sofic.automata.transducer_operations import transduce_generator
|
|
191
|
+
|
|
192
|
+
return transduce_generator(self, generator, **kwargs)
|
|
193
|
+
|
|
194
|
+
def to_sofic_relation(self) -> Any:
|
|
195
|
+
"""Return the topological support as a product-alphabet sofic relation."""
|
|
196
|
+
from sofic.shifts.sofic_relation import SoficRelation
|
|
197
|
+
|
|
198
|
+
return SoficRelation.from_transducer(self)
|
|
199
|
+
|
|
200
|
+
def to_textile_system(self) -> Any:
|
|
201
|
+
"""Return this transducer as a textile system (Nasu 1995)."""
|
|
202
|
+
from sofic.shifts.textile import TextileSystem
|
|
203
|
+
|
|
204
|
+
return TextileSystem.from_transducer(self)
|
|
205
|
+
|
|
206
|
+
def to_wfst(self, *, semiring: str = "probability") -> Any:
|
|
207
|
+
"""Return this transducer as a weighted finite-state transducer."""
|
|
208
|
+
from sofic.automata.subsequential import WeightedFiniteStateTransducer
|
|
209
|
+
|
|
210
|
+
return WeightedFiniteStateTransducer.from_transducer(self, semiring=semiring)
|
|
211
|
+
|
|
212
|
+
def to_sliding_block_code(self, *, max_window: int = 4) -> Any:
|
|
213
|
+
"""Return the sliding block code realized by this transducer, if it is one.
|
|
214
|
+
|
|
215
|
+
Requires deterministic (right-resolving) outputs with a finite window;
|
|
216
|
+
raises :class:`ValueError` otherwise (e.g. for stochastic channels).
|
|
217
|
+
"""
|
|
218
|
+
from sofic.shifts.textile import TextileSystem
|
|
219
|
+
|
|
220
|
+
return TextileSystem.from_transducer(self).induced_code(max_window=max_window)
|
|
221
|
+
|
|
222
|
+
def labeled_transition_matrices(
|
|
223
|
+
self,
|
|
224
|
+
node_ordering: Sequence[Hashable] | None = None,
|
|
225
|
+
*,
|
|
226
|
+
mode: str = "zero",
|
|
227
|
+
) -> tuple[dict[tuple[Any, Any], np.ndarray], list[Hashable]] | dict[tuple[Any, Any], np.ndarray]:
|
|
228
|
+
"""Return matrices keyed by ``(input, output)`` symbol pairs."""
|
|
229
|
+
norder = list(self.states()) if node_ordering is None else list(node_ordering)
|
|
230
|
+
missing = [state for state in norder if not self.graph.has_state(state)]
|
|
231
|
+
if missing:
|
|
232
|
+
raise ValueError(f"unknown states in node_ordering: {missing!r}")
|
|
233
|
+
|
|
234
|
+
if mode not in {"zero", "nan"}:
|
|
235
|
+
raise ValueError("mode must be 'zero' or 'nan'")
|
|
236
|
+
|
|
237
|
+
idx = {state: i for i, state in enumerate(norder)}
|
|
238
|
+
n = len(norder)
|
|
239
|
+
input_by_state, _ = self.alphabets(node=True)
|
|
240
|
+
fill = 0.0 if mode == "zero" else np.nan
|
|
241
|
+
|
|
242
|
+
def default_matrix(input_symbol: Any) -> np.ndarray:
|
|
243
|
+
matrix = np.zeros((n, n), dtype=float)
|
|
244
|
+
if mode == "nan":
|
|
245
|
+
for state in norder:
|
|
246
|
+
if input_symbol not in input_by_state.get(state, frozenset()):
|
|
247
|
+
matrix[idx[state]] = fill
|
|
248
|
+
return matrix
|
|
249
|
+
|
|
250
|
+
matrices: dict[tuple[Any, Any], np.ndarray] = {}
|
|
251
|
+
for transition in self.transitions():
|
|
252
|
+
if transition.source not in idx or transition.target not in idx:
|
|
253
|
+
continue
|
|
254
|
+
input_symbol = transition.data.get(ATTR_SYMBOL, EPSILON)
|
|
255
|
+
output_symbol = transition.data.get(ATTR_OUTPUT, EPSILON)
|
|
256
|
+
key = (input_symbol, output_symbol)
|
|
257
|
+
if key not in matrices:
|
|
258
|
+
matrices[key] = default_matrix(input_symbol)
|
|
259
|
+
matrices[key][idx[transition.source], idx[transition.target]] += _transition_probability(transition.data)
|
|
260
|
+
|
|
261
|
+
if node_ordering is None:
|
|
262
|
+
return matrices, norder
|
|
263
|
+
return matrices
|
|
264
|
+
|
|
265
|
+
def validate_stochastic(self) -> None:
|
|
266
|
+
"""Validate probability rows grouped by ``(state, input symbol)``."""
|
|
267
|
+
rows: dict[tuple[Hashable, Any], float] = defaultdict(float)
|
|
268
|
+
for transition in self.transitions():
|
|
269
|
+
prob = _transition_probability(transition.data)
|
|
270
|
+
if prob < 0:
|
|
271
|
+
raise StochasticValidationError(f"negative transducer probability on {transition}")
|
|
272
|
+
rows[(transition.source, transition.data.get(ATTR_SYMBOL, EPSILON))] += prob
|
|
273
|
+
for (state, symbol), total in rows.items():
|
|
274
|
+
if not np.isclose(total, 1.0):
|
|
275
|
+
raise StochasticValidationError(f"transducer row ({state!r}, {symbol!r}) sums to {total}, not 1")
|
|
276
|
+
|
|
277
|
+
def validate(self) -> None:
|
|
278
|
+
super().validate()
|
|
279
|
+
for transition in self.transitions():
|
|
280
|
+
if ATTR_OUTPUT in transition.data:
|
|
281
|
+
out = transition.data[ATTR_OUTPUT]
|
|
282
|
+
if out is not EPSILON:
|
|
283
|
+
self._require(out in self.output_alphabet, f"output {out!r} not in output alphabet")
|
|
284
|
+
if ATTR_SYMBOL in transition.data:
|
|
285
|
+
sym = transition.data[ATTR_SYMBOL]
|
|
286
|
+
if sym is not EPSILON:
|
|
287
|
+
self._require(sym in self.input_alphabet, f"symbol {sym!r} not in input alphabet")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class MooreMachine(Transducer):
|
|
291
|
+
"""Output symbols on states."""
|
|
292
|
+
|
|
293
|
+
def add_transition(
|
|
294
|
+
self,
|
|
295
|
+
source: Hashable,
|
|
296
|
+
target: Hashable,
|
|
297
|
+
symbol: Any,
|
|
298
|
+
*,
|
|
299
|
+
prob: float | None = None,
|
|
300
|
+
**attrs: Any,
|
|
301
|
+
) -> int:
|
|
302
|
+
"""Add a transition with input ``symbol``."""
|
|
303
|
+
data = {ATTR_SYMBOL: symbol, **attrs}
|
|
304
|
+
if prob is not None:
|
|
305
|
+
data[ATTR_PROB] = float(prob)
|
|
306
|
+
return self.graph.add_transition(source, target, **data)
|
|
307
|
+
|
|
308
|
+
def set_output(self, state: Hashable, output: Any) -> None:
|
|
309
|
+
"""Set a state's Moore output."""
|
|
310
|
+
self.graph.nx.nodes[state][ATTR_OUTPUT] = output
|
|
311
|
+
|
|
312
|
+
def transduce(self, word: Sequence[Any]) -> set[tuple[Any, ...]]:
|
|
313
|
+
from sofic.automata.transducer_simulation import transduce_moore
|
|
314
|
+
|
|
315
|
+
return transduce_moore(self, word)
|
|
316
|
+
|
|
317
|
+
def validate(self) -> None:
|
|
318
|
+
super().validate()
|
|
319
|
+
for state in self.states():
|
|
320
|
+
out = self.graph.state_attrs(state).get(ATTR_OUTPUT)
|
|
321
|
+
if out is not None and out is not EPSILON:
|
|
322
|
+
self._require(out in self.output_alphabet, f"output {out!r} not in output alphabet")
|
|
323
|
+
for transition in self.transitions():
|
|
324
|
+
if ATTR_SYMBOL in transition.data:
|
|
325
|
+
sym = transition.data[ATTR_SYMBOL]
|
|
326
|
+
if sym is not EPSILON:
|
|
327
|
+
self._require(sym in self.input_alphabet, f"symbol {sym!r} not in input alphabet")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _transition_probability(data: Mapping[str, Any]) -> float:
|
|
331
|
+
return float(data.get(ATTR_PROB, 1.0))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _partial_machine(machine: MealyMachine, label_attr: str, *, build: bool) -> Any:
|
|
335
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
336
|
+
from sofic.generators.mealy import MealyHMM
|
|
337
|
+
|
|
338
|
+
states = tuple(machine.states())
|
|
339
|
+
initial_states = tuple(machine.initial_states) or states
|
|
340
|
+
initial = {state: 1.0 / len(initial_states) for state in initial_states} if initial_states else {}
|
|
341
|
+
alphabet = frozenset(
|
|
342
|
+
transition.data[label_attr]
|
|
343
|
+
for transition in machine.transitions()
|
|
344
|
+
if label_attr in transition.data and transition.data[label_attr] is not EPSILON
|
|
345
|
+
)
|
|
346
|
+
result = MealyHMM(initial_distribution=initial, observation_alphabet=alphabet)
|
|
347
|
+
for state in states:
|
|
348
|
+
result.graph.add_state(state)
|
|
349
|
+
|
|
350
|
+
row_counts: dict[Hashable, int] = defaultdict(int)
|
|
351
|
+
for transition in machine.transitions():
|
|
352
|
+
symbol = transition.data.get(label_attr)
|
|
353
|
+
if symbol is None or symbol is EPSILON:
|
|
354
|
+
continue
|
|
355
|
+
row_counts[transition.source] += 1
|
|
356
|
+
for transition in machine.transitions():
|
|
357
|
+
symbol = transition.data.get(label_attr)
|
|
358
|
+
if symbol is None or symbol is EPSILON:
|
|
359
|
+
continue
|
|
360
|
+
prob = 1.0 / row_counts[transition.source] if row_counts[transition.source] else 0.0
|
|
361
|
+
result.graph.add_transition(transition.source, transition.target, **{ATTR_EMISSION: symbol, ATTR_PROB: prob})
|
|
362
|
+
|
|
363
|
+
if build:
|
|
364
|
+
return EpsilonMachine.from_hmm(result)
|
|
365
|
+
return result
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Unifilar labeled automata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.automata.base import LabeledAutomaton
|
|
9
|
+
from sofic.exceptions import UnifilarityError
|
|
10
|
+
from sofic.graph import ATTR_SYMBOL, EPSILON
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnifilarAutomaton(LabeledAutomaton):
|
|
14
|
+
"""At most one outgoing edge per (state, symbol), epsilon excluded."""
|
|
15
|
+
|
|
16
|
+
def is_unifilar(self) -> bool:
|
|
17
|
+
"""Return whether this presentation is right-resolving (unifilar)."""
|
|
18
|
+
from sofic.properties import is_unifilar_symbols
|
|
19
|
+
|
|
20
|
+
return is_unifilar_symbols(self)
|
|
21
|
+
|
|
22
|
+
def validate(self) -> None:
|
|
23
|
+
super().validate()
|
|
24
|
+
if self.is_unifilar():
|
|
25
|
+
return
|
|
26
|
+
seen: set[tuple[Hashable, Any]] = set()
|
|
27
|
+
for transition in self.transitions():
|
|
28
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
29
|
+
if symbol is EPSILON or symbol is None:
|
|
30
|
+
continue
|
|
31
|
+
key = (transition.source, symbol)
|
|
32
|
+
if key in seen:
|
|
33
|
+
raise UnifilarityError(f"duplicate unifilar transition on {key}")
|
|
34
|
+
seen.add(key)
|
|
35
|
+
|
|
36
|
+
def recognizes(self, word: Sequence[Any]) -> bool:
|
|
37
|
+
return bool(self._run_nfa(word) & self.accepting_states)
|
|
38
|
+
|
|
39
|
+
def markov_order(self) -> int | float:
|
|
40
|
+
"""Markov order ``R`` for this right-resolving (unifilar) presentation."""
|
|
41
|
+
from sofic.generators.synchronization import graph_from_unifilar_automaton, markov_order_from_graph
|
|
42
|
+
|
|
43
|
+
return markov_order_from_graph(graph_from_unifilar_automaton(self))
|
|
44
|
+
|
|
45
|
+
def cryptic_order(self) -> int | float:
|
|
46
|
+
"""Cryptic order ``k_chi`` for this right-resolving presentation."""
|
|
47
|
+
from sofic.generators.synchronization import cryptic_order_from_graph, graph_from_unifilar_automaton
|
|
48
|
+
|
|
49
|
+
return cryptic_order_from_graph(graph_from_unifilar_automaton(self))
|
|
50
|
+
|
|
51
|
+
def reset_threshold(self) -> int | float:
|
|
52
|
+
"""Reset threshold: shortest synchronizing (reset) word length.
|
|
53
|
+
|
|
54
|
+
The complement of :meth:`markov_order` (shortest vs longest start-to-
|
|
55
|
+
singleton path); ``math.inf`` when not exactly synchronizable. For the
|
|
56
|
+
Cerny bound (complete automata only) see Volkov (2008).
|
|
57
|
+
"""
|
|
58
|
+
from sofic.generators.synchronization import graph_from_unifilar_automaton, reset_threshold_from_graph
|
|
59
|
+
|
|
60
|
+
return reset_threshold_from_graph(graph_from_unifilar_automaton(self))
|
|
61
|
+
|
|
62
|
+
def synchronizing_word(self) -> list[Any] | None:
|
|
63
|
+
"""Return a shortest synchronizing word, or ``None`` if not exactly synchronizable.
|
|
64
|
+
|
|
65
|
+
The list of symbols has length :meth:`reset_threshold` (Travers &
|
|
66
|
+
Crutchfield, arXiv:1008.4182).
|
|
67
|
+
"""
|
|
68
|
+
from sofic.generators.synchronization import (
|
|
69
|
+
graph_from_unifilar_automaton,
|
|
70
|
+
shortest_synchronizing_word_from_graph,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return shortest_synchronizing_word_from_graph(graph_from_unifilar_automaton(self))
|
|
74
|
+
|
|
75
|
+
def is_exactly_synchronizable(self) -> bool:
|
|
76
|
+
"""Return whether this presentation is exactly synchronizable.
|
|
77
|
+
|
|
78
|
+
True iff a finite synchronizing word exists (the reset threshold is
|
|
79
|
+
finite; Travers & Crutchfield, arXiv:1008.4182). Strictly weaker than
|
|
80
|
+
finite Markov order: see :meth:`is_definite`.
|
|
81
|
+
"""
|
|
82
|
+
from sofic.generators.synchronization import graph_from_unifilar_automaton, is_exactly_synchronizable
|
|
83
|
+
|
|
84
|
+
return is_exactly_synchronizable(graph_from_unifilar_automaton(self))
|
|
85
|
+
|
|
86
|
+
def is_asymptotically_synchronizable(self) -> bool:
|
|
87
|
+
"""Return whether this presentation is asymptotically synchronizable.
|
|
88
|
+
|
|
89
|
+
True for every finite-state ε-machine (Travers & Crutchfield,
|
|
90
|
+
arXiv:1008.4182); theorem-backed rather than computed.
|
|
91
|
+
"""
|
|
92
|
+
from sofic.generators.synchronization import (
|
|
93
|
+
graph_from_unifilar_automaton,
|
|
94
|
+
is_asymptotically_synchronizable_from_graph,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return is_asymptotically_synchronizable_from_graph(graph_from_unifilar_automaton(self))
|
|
98
|
+
|
|
99
|
+
def is_definite(self) -> bool:
|
|
100
|
+
"""Return whether this presentation is a definite automaton (finite Markov order).
|
|
101
|
+
|
|
102
|
+
Definiteness (Perles-Rabin-Shamir) means the state is fixed by the last
|
|
103
|
+
``R`` symbols. Implies :meth:`is_exactly_synchronizable`.
|
|
104
|
+
"""
|
|
105
|
+
from sofic.generators.synchronization import graph_from_unifilar_automaton, is_definite_from_graph
|
|
106
|
+
|
|
107
|
+
return is_definite_from_graph(graph_from_unifilar_automaton(self))
|