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,407 @@
|
|
|
1
|
+
"""Topological synchronization orders for unifilar generators.
|
|
2
|
+
|
|
3
|
+
Markov order and cryptic order are computed via the power-automaton algorithms
|
|
4
|
+
of James, Mahoney, Ellison & Crutchfield (arXiv:1010.5545). Both depend only
|
|
5
|
+
on unifilar graph topology, not transition probabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from collections.abc import Hashable, Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import networkx as nx
|
|
16
|
+
|
|
17
|
+
from sofic.exceptions import UnifilarityError
|
|
18
|
+
from sofic.graph import ATTR_EMISSION, ATTR_SYMBOL
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class TopologicalUnifilarGraph:
|
|
23
|
+
"""Deterministic edge-labeled transition system (right-resolving)."""
|
|
24
|
+
|
|
25
|
+
states: frozenset[Hashable]
|
|
26
|
+
alphabet: frozenset[Any]
|
|
27
|
+
transitions: Mapping[tuple[Hashable, Any], Hashable]
|
|
28
|
+
|
|
29
|
+
def delta(self, state: Hashable, symbol: Any) -> Hashable | None:
|
|
30
|
+
return self.transitions.get((state, symbol))
|
|
31
|
+
|
|
32
|
+
def delta_set(self, states: frozenset[Hashable], symbol: Any) -> frozenset[Hashable]:
|
|
33
|
+
successors: set[Hashable] = set()
|
|
34
|
+
for state in states:
|
|
35
|
+
target = self.delta(state, symbol)
|
|
36
|
+
if target is not None:
|
|
37
|
+
successors.add(target)
|
|
38
|
+
return frozenset(successors)
|
|
39
|
+
|
|
40
|
+
def is_recurrent_pa_state(self, pa_state: frozenset[Hashable]) -> bool:
|
|
41
|
+
return len(pa_state) == 1 and next(iter(pa_state)) in self.states
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class PowerAutomaton:
|
|
46
|
+
"""Subset automaton over a :class:`TopologicalUnifilarGraph`."""
|
|
47
|
+
|
|
48
|
+
graph: TopologicalUnifilarGraph
|
|
49
|
+
start: frozenset[Hashable]
|
|
50
|
+
transitions: dict[frozenset[Hashable], dict[Any, frozenset[Hashable]]] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def is_transient(self, pa_state: frozenset[Hashable]) -> bool:
|
|
53
|
+
return not self.graph.is_recurrent_pa_state(pa_state)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_topological_graph_from_transitions(
|
|
57
|
+
states: frozenset[Hashable],
|
|
58
|
+
alphabet: frozenset[Any],
|
|
59
|
+
transitions: Mapping[tuple[Hashable, Any], Hashable],
|
|
60
|
+
) -> TopologicalUnifilarGraph:
|
|
61
|
+
return TopologicalUnifilarGraph(states=states, alphabet=alphabet, transitions=dict(transitions))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def power_automaton(graph: TopologicalUnifilarGraph) -> PowerAutomaton:
|
|
65
|
+
"""Build the power automaton via subset construction from the full state set."""
|
|
66
|
+
start = frozenset(graph.states)
|
|
67
|
+
pa = PowerAutomaton(graph=graph, start=start)
|
|
68
|
+
queue = [start]
|
|
69
|
+
seen = {start}
|
|
70
|
+
while queue:
|
|
71
|
+
current = queue.pop(0)
|
|
72
|
+
pa.transitions.setdefault(current, {})
|
|
73
|
+
for symbol in graph.alphabet:
|
|
74
|
+
successor = graph.delta_set(current, symbol)
|
|
75
|
+
if not successor:
|
|
76
|
+
continue
|
|
77
|
+
pa.transitions[current][symbol] = successor
|
|
78
|
+
if successor not in seen:
|
|
79
|
+
seen.add(successor)
|
|
80
|
+
queue.append(successor)
|
|
81
|
+
return pa
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _power_automaton_digraph(pa: PowerAutomaton) -> nx.MultiDiGraph:
|
|
85
|
+
"""Materialize the power automaton as a networkx graph.
|
|
86
|
+
|
|
87
|
+
Nodes are power-automaton states (frozensets of graph states). Each edge
|
|
88
|
+
carries the emitted ``symbol`` that drives the subset transition and a
|
|
89
|
+
``weight`` of ``-1`` when the source is still unsynchronized (a non-
|
|
90
|
+
singleton subset) and ``0`` once synchronized (a singleton). The negative
|
|
91
|
+
transient weight turns the *longest* synchronizing path (Markov order) into
|
|
92
|
+
a shortest-path problem solvable by Bellman-Ford. A
|
|
93
|
+
:class:`networkx.MultiDiGraph` is used because two subsets can be joined by
|
|
94
|
+
parallel edges labeled with different symbols.
|
|
95
|
+
"""
|
|
96
|
+
digraph = nx.MultiDiGraph()
|
|
97
|
+
digraph.add_node(pa.start)
|
|
98
|
+
for source, out_map in pa.transitions.items():
|
|
99
|
+
weight = -1 if pa.is_transient(source) else 0
|
|
100
|
+
for symbol, target in out_map.items():
|
|
101
|
+
digraph.add_edge(source, target, symbol=symbol, weight=weight)
|
|
102
|
+
return digraph
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _states_reaching_synchronization(
|
|
106
|
+
digraph: nx.MultiDiGraph, singletons: set[frozenset[Hashable]]
|
|
107
|
+
) -> set[frozenset[Hashable]]:
|
|
108
|
+
"""Return the singletons together with every node that can reach one.
|
|
109
|
+
|
|
110
|
+
Computed as the ancestors of a virtual sink wired to every singleton, so a
|
|
111
|
+
single reverse traversal covers all synchronized targets at once.
|
|
112
|
+
"""
|
|
113
|
+
if not singletons:
|
|
114
|
+
return set()
|
|
115
|
+
reverse = digraph.reverse(copy=True)
|
|
116
|
+
sink = object()
|
|
117
|
+
for node in singletons:
|
|
118
|
+
reverse.add_edge(sink, node)
|
|
119
|
+
return nx.descendants(reverse, sink)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _bellman_ford_longest_transient_path(pa: PowerAutomaton) -> float:
|
|
123
|
+
"""Return longest prefix-free synchronizing word length, or ``math.inf``.
|
|
124
|
+
|
|
125
|
+
Restricts the power automaton to the nodes that can still reach a singleton
|
|
126
|
+
(so a transient loop that never synchronizes is not mistaken for one that
|
|
127
|
+
does), then solves the negative-weight shortest-path problem with networkx's
|
|
128
|
+
Bellman-Ford. A negative cycle reachable from the start within that
|
|
129
|
+
subgraph is an unbounded transient loop on a synchronizing path, i.e. an
|
|
130
|
+
infinite Markov order, which networkx signals via ``NetworkXUnbounded``.
|
|
131
|
+
"""
|
|
132
|
+
digraph = _power_automaton_digraph(pa)
|
|
133
|
+
singletons = {node for node in digraph if pa.graph.is_recurrent_pa_state(node)}
|
|
134
|
+
sync_reachable = _states_reaching_synchronization(digraph, singletons)
|
|
135
|
+
if pa.start not in sync_reachable:
|
|
136
|
+
return math.inf
|
|
137
|
+
|
|
138
|
+
subgraph = digraph.subgraph(sync_reachable)
|
|
139
|
+
try:
|
|
140
|
+
dist = nx.single_source_bellman_ford_path_length(subgraph, pa.start, weight="weight")
|
|
141
|
+
except nx.NetworkXUnbounded:
|
|
142
|
+
return math.inf
|
|
143
|
+
|
|
144
|
+
best = min((dist[node] for node in singletons if node in dist), default=math.inf)
|
|
145
|
+
if best == math.inf:
|
|
146
|
+
return math.inf
|
|
147
|
+
return -best
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def markov_order_from_graph(graph: TopologicalUnifilarGraph) -> int | float:
|
|
151
|
+
"""Longest prefix-free synchronizing word length (Markov order ``R``)."""
|
|
152
|
+
if not graph.states:
|
|
153
|
+
return 0
|
|
154
|
+
pa = power_automaton(graph)
|
|
155
|
+
order = _bellman_ford_longest_transient_path(pa)
|
|
156
|
+
if order == math.inf:
|
|
157
|
+
return math.inf
|
|
158
|
+
return int(order)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def reset_threshold_from_graph(graph: TopologicalUnifilarGraph) -> int | float:
|
|
162
|
+
"""Reset threshold: length of the *shortest* synchronizing word.
|
|
163
|
+
|
|
164
|
+
This is the shortest path from the full-support start of the power
|
|
165
|
+
automaton to any singleton (synchronized) state -- the complement of the
|
|
166
|
+
Markov order, which is the *longest* such prefix-free path. Returns ``0``
|
|
167
|
+
for a single-state machine and ``math.inf`` when no finite synchronizing
|
|
168
|
+
word exists (i.e. the presentation is not exactly synchronizable).
|
|
169
|
+
|
|
170
|
+
The shortest-reset-word length is the quantity bounded by the Cerny
|
|
171
|
+
conjecture for complete deterministic automata :cite:`Cerny1964`
|
|
172
|
+
:cite:`Volkov2008`; note that right-resolving epsilon-machine presentations
|
|
173
|
+
are generally *partial*, so that quadratic bound does not apply here. Like
|
|
174
|
+
the Markov order, it is a topological property computed from the power
|
|
175
|
+
automaton :cite:`James2010`.
|
|
176
|
+
"""
|
|
177
|
+
if not graph.states:
|
|
178
|
+
return 0
|
|
179
|
+
pa = power_automaton(graph)
|
|
180
|
+
lengths = nx.single_source_shortest_path_length(_power_automaton_digraph(pa), pa.start)
|
|
181
|
+
singleton_lengths = [length for node, length in lengths.items() if pa.graph.is_recurrent_pa_state(node)]
|
|
182
|
+
return min(singleton_lengths, default=math.inf)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def shortest_synchronizing_word_from_graph(
|
|
186
|
+
graph: TopologicalUnifilarGraph,
|
|
187
|
+
) -> list[Any] | None:
|
|
188
|
+
"""Return a shortest synchronizing word, or ``None`` if none exists.
|
|
189
|
+
|
|
190
|
+
A synchronizing (reset) word drives the observer's belief to a single
|
|
191
|
+
state regardless of the start state :cite:`Travers2010`. The returned list
|
|
192
|
+
of emitted symbols has length :func:`reset_threshold_from_graph`; it is
|
|
193
|
+
empty for an already-synchronized single-state machine and ``None`` when
|
|
194
|
+
the presentation is not exactly synchronizable.
|
|
195
|
+
"""
|
|
196
|
+
if not graph.states:
|
|
197
|
+
return []
|
|
198
|
+
pa = power_automaton(graph)
|
|
199
|
+
digraph = _power_automaton_digraph(pa)
|
|
200
|
+
node_paths = nx.single_source_shortest_path(digraph, pa.start)
|
|
201
|
+
best_path: list[frozenset[Hashable]] | None = None
|
|
202
|
+
for node, node_path in node_paths.items():
|
|
203
|
+
if pa.graph.is_recurrent_pa_state(node) and (best_path is None or len(node_path) < len(best_path)):
|
|
204
|
+
best_path = node_path
|
|
205
|
+
if best_path is None:
|
|
206
|
+
return None
|
|
207
|
+
ordered_symbols = sorted(graph.alphabet, key=repr)
|
|
208
|
+
word: list[Any] = []
|
|
209
|
+
for source, target in zip(best_path, best_path[1:], strict=False):
|
|
210
|
+
symbol = next(sym for sym in ordered_symbols if graph.delta_set(source, sym) == target)
|
|
211
|
+
word.append(symbol)
|
|
212
|
+
return word
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _predecessors_on_symbol(graph: TopologicalUnifilarGraph, target: Hashable, symbol: Any) -> frozenset[Hashable]:
|
|
216
|
+
return frozenset(state for state in graph.states if graph.delta(state, symbol) == target)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _ensure_pa_state(
|
|
220
|
+
transitions: dict[frozenset[Hashable], dict[Any, frozenset[Hashable]]],
|
|
221
|
+
states: set[frozenset[Hashable]],
|
|
222
|
+
graph: TopologicalUnifilarGraph,
|
|
223
|
+
pa_state: frozenset[Hashable],
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Ensure ``pa_state`` and its subset successors are present in the PA."""
|
|
226
|
+
queue = [pa_state]
|
|
227
|
+
while queue:
|
|
228
|
+
current = queue.pop(0)
|
|
229
|
+
if current in transitions:
|
|
230
|
+
continue
|
|
231
|
+
out_map: dict[Any, frozenset[Hashable]] = {}
|
|
232
|
+
for symbol in graph.alphabet:
|
|
233
|
+
successor = graph.delta_set(current, symbol)
|
|
234
|
+
if successor:
|
|
235
|
+
out_map[symbol] = successor
|
|
236
|
+
if successor not in transitions:
|
|
237
|
+
queue.append(successor)
|
|
238
|
+
transitions[current] = out_map
|
|
239
|
+
states.add(current)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _refine_cryptic_pa(pa: PowerAutomaton) -> PowerAutomaton:
|
|
243
|
+
"""Apply veracity refinement (James et al., Sec. VI.1) until quiescent."""
|
|
244
|
+
graph = pa.graph
|
|
245
|
+
transitions = {state: dict(out_map) for state, out_map in pa.transitions.items()}
|
|
246
|
+
states = set(transitions)
|
|
247
|
+
|
|
248
|
+
changed = True
|
|
249
|
+
while changed:
|
|
250
|
+
changed = False
|
|
251
|
+
for source in list(states):
|
|
252
|
+
out_map = transitions.get(source, {})
|
|
253
|
+
for symbol, target in list(out_map.items()):
|
|
254
|
+
if not graph.is_recurrent_pa_state(target):
|
|
255
|
+
continue
|
|
256
|
+
target_state = next(iter(target))
|
|
257
|
+
true_sources = _predecessors_on_symbol(graph, target_state, symbol) & source
|
|
258
|
+
if not true_sources:
|
|
259
|
+
del out_map[symbol]
|
|
260
|
+
changed = True
|
|
261
|
+
continue
|
|
262
|
+
if true_sources == source:
|
|
263
|
+
continue
|
|
264
|
+
_ensure_pa_state(transitions, states, graph, true_sources)
|
|
265
|
+
refined_out = transitions.setdefault(true_sources, {})
|
|
266
|
+
if refined_out.get(symbol) != target:
|
|
267
|
+
refined_out[symbol] = target
|
|
268
|
+
changed = True
|
|
269
|
+
if source != pa.start and symbol in out_map:
|
|
270
|
+
del out_map[symbol]
|
|
271
|
+
changed = True
|
|
272
|
+
if not out_map:
|
|
273
|
+
transitions.pop(source, None)
|
|
274
|
+
states.discard(source)
|
|
275
|
+
|
|
276
|
+
return PowerAutomaton(graph=graph, start=pa.start, transitions=transitions)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _cryptic_order_from_refined_pa(pa: PowerAutomaton) -> int | float:
|
|
280
|
+
refined = _refine_cryptic_pa(pa)
|
|
281
|
+
order = _bellman_ford_longest_transient_path(refined)
|
|
282
|
+
if order == math.inf:
|
|
283
|
+
return math.inf
|
|
284
|
+
return int(order)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cryptic_order_from_graph(graph: TopologicalUnifilarGraph) -> int | float:
|
|
288
|
+
"""Cryptic order ``k_chi`` via refined power automaton (James et al., Sec. VI)."""
|
|
289
|
+
if not graph.states:
|
|
290
|
+
return 0
|
|
291
|
+
pa = power_automaton(graph)
|
|
292
|
+
markov_order = _bellman_ford_longest_transient_path(pa)
|
|
293
|
+
order = _cryptic_order_from_refined_pa(pa)
|
|
294
|
+
if markov_order != math.inf and (order == math.inf or order > markov_order):
|
|
295
|
+
return int(markov_order)
|
|
296
|
+
if order == math.inf:
|
|
297
|
+
return math.inf
|
|
298
|
+
return int(order)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def is_exactly_synchronizable(graph: TopologicalUnifilarGraph) -> bool:
|
|
302
|
+
"""Return whether the presentation is exactly synchronizable.
|
|
303
|
+
|
|
304
|
+
An epsilon-machine is *exact* iff it has some finite synchronizing word,
|
|
305
|
+
equivalently iff ``Pr(SYN(M)) = 1`` -- the observer synchronizes to the
|
|
306
|
+
hidden state in finite time for almost every generated sequence
|
|
307
|
+
:cite:`Travers2010`. This holds iff the reset threshold is finite. Note
|
|
308
|
+
this is strictly weaker than finite Markov order (see
|
|
309
|
+
:func:`is_definite_from_graph`): the butterfly process is exact yet has
|
|
310
|
+
infinite Markov order.
|
|
311
|
+
"""
|
|
312
|
+
return reset_threshold_from_graph(graph) != math.inf
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def is_definite_from_graph(graph: TopologicalUnifilarGraph) -> bool:
|
|
316
|
+
"""Return whether the presentation is a definite automaton (finite Markov order).
|
|
317
|
+
|
|
318
|
+
A deterministic automaton is *definite* of degree ``R`` when the current
|
|
319
|
+
state is fixed by the last ``R`` symbols regardless of the start state
|
|
320
|
+
:cite:`Cerny1964`; this coincides with finite Markov order ``R`` for a
|
|
321
|
+
right-resolving epsilon-machine :cite:`James2010`. Definiteness implies
|
|
322
|
+
exact synchronizability, but not conversely.
|
|
323
|
+
"""
|
|
324
|
+
return markov_order_from_graph(graph) != math.inf
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def is_asymptotically_synchronizable_from_graph(graph: TopologicalUnifilarGraph) -> bool:
|
|
328
|
+
"""Return whether the presentation is asymptotically synchronizable.
|
|
329
|
+
|
|
330
|
+
An epsilon-machine is asymptotically synchronizable iff the observer's
|
|
331
|
+
state uncertainty vanishes as ``L -> infinity`` for almost every generated
|
|
332
|
+
sequence (``Pr(WSYN(M)) = 1``) :cite:`Travers2010`. Every finite-state
|
|
333
|
+
epsilon-machine has this property, so this returns ``True`` for any
|
|
334
|
+
non-empty presentation -- it is theorem-backed rather than computed.
|
|
335
|
+
"""
|
|
336
|
+
return bool(graph.states)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def graph_from_epsilon_machine(eps: Any) -> TopologicalUnifilarGraph:
|
|
340
|
+
"""Strip probabilities from an ε-machine into a topological graph."""
|
|
341
|
+
from sofic.properties import is_unifilar_emissions
|
|
342
|
+
|
|
343
|
+
if not is_unifilar_emissions(eps):
|
|
344
|
+
for transition in eps.transitions():
|
|
345
|
+
emission = transition.data.get(ATTR_EMISSION)
|
|
346
|
+
if emission is None:
|
|
347
|
+
continue
|
|
348
|
+
key = (transition.source, emission)
|
|
349
|
+
for other in eps.graph.out_transitions(transition.source):
|
|
350
|
+
other_emission = other.data.get(ATTR_EMISSION)
|
|
351
|
+
if other_emission == emission and other.target != transition.target:
|
|
352
|
+
raise UnifilarityError(f"non-unifilar duplicate emission {emission!r} from {transition.source!r}")
|
|
353
|
+
|
|
354
|
+
transitions: dict[tuple[Hashable, Any], Hashable] = {}
|
|
355
|
+
alphabet: set[Any] = set()
|
|
356
|
+
for transition in eps.transitions():
|
|
357
|
+
emission = transition.data.get(ATTR_EMISSION)
|
|
358
|
+
if emission is None:
|
|
359
|
+
continue
|
|
360
|
+
key = (transition.source, emission)
|
|
361
|
+
if key in transitions and transitions[key] != transition.target:
|
|
362
|
+
raise UnifilarityError(f"non-unifilar duplicate emission {emission!r} from {transition.source!r}")
|
|
363
|
+
transitions[key] = transition.target
|
|
364
|
+
alphabet.add(emission)
|
|
365
|
+
states = frozenset(eps.states())
|
|
366
|
+
if eps.observation_alphabet:
|
|
367
|
+
alphabet.update(eps.observation_alphabet)
|
|
368
|
+
return TopologicalUnifilarGraph(
|
|
369
|
+
states=states,
|
|
370
|
+
alphabet=frozenset(alphabet),
|
|
371
|
+
transitions=transitions,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def graph_from_unifilar_automaton(aut: Any) -> TopologicalUnifilarGraph:
|
|
376
|
+
"""Build a topological graph from a :class:`~sofic.automata.unifilar.UnifilarAutomaton`."""
|
|
377
|
+
from sofic.graph import EPSILON
|
|
378
|
+
|
|
379
|
+
transitions: dict[tuple[Hashable, Any], Hashable] = {}
|
|
380
|
+
alphabet: set[Any] = set()
|
|
381
|
+
for transition in aut.transitions():
|
|
382
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
383
|
+
if symbol is None or symbol is EPSILON:
|
|
384
|
+
continue
|
|
385
|
+
key = (transition.source, symbol)
|
|
386
|
+
if key in transitions and transitions[key] != transition.target:
|
|
387
|
+
raise UnifilarityError(f"non-unifilar duplicate symbol {symbol!r} from {transition.source!r}")
|
|
388
|
+
transitions[key] = transition.target
|
|
389
|
+
alphabet.add(symbol)
|
|
390
|
+
states = frozenset(aut.states())
|
|
391
|
+
if aut.input_alphabet:
|
|
392
|
+
alphabet.update(aut.input_alphabet)
|
|
393
|
+
return TopologicalUnifilarGraph(
|
|
394
|
+
states=states,
|
|
395
|
+
alphabet=frozenset(alphabet),
|
|
396
|
+
transitions=transitions,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def graph_from_sofic_shift(shift: Any) -> TopologicalUnifilarGraph:
|
|
401
|
+
"""Build a topological graph from a right-resolving sofic presentation."""
|
|
402
|
+
graph = graph_from_unifilar_automaton(shift)
|
|
403
|
+
if shift.symbol_alphabet:
|
|
404
|
+
missing = graph.alphabet - shift.symbol_alphabet
|
|
405
|
+
if missing:
|
|
406
|
+
raise UnifilarityError(f"shift alphabet does not cover transition symbols: {missing}")
|
|
407
|
+
return graph
|