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/edsm.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Passive DFA learning via Evidence-Driven State Merging (blue-fringe).
|
|
2
|
+
|
|
3
|
+
EDSM upgrades the greedy RPNI merge order (:func:`sofic.automata.rpni.learn_dfa_rpni`)
|
|
4
|
+
with the evidence-driven, red/blue "blue-fringe" strategy that won the Abbadingo
|
|
5
|
+
One competition :cite:`Lang1998`. Starting from the augmented prefix-tree
|
|
6
|
+
acceptor of the labeled sample, it maintains a set of confirmed *red* states and
|
|
7
|
+
their *blue* fringe; at each step it either promotes a blue state that cannot be
|
|
8
|
+
merged with any red state, or commits the single highest-*evidence* merge, where
|
|
9
|
+
the evidence of a merge is the number of identically-labeled state pairs it
|
|
10
|
+
folds together. The result is the practical state-of-the-art passive DFA
|
|
11
|
+
heuristic and, like RPNI, is consistent with the sample.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Hashable, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from sofic.automata.dfa import DFA
|
|
21
|
+
|
|
22
|
+
__all__ = ["learn_dfa_edsm"]
|
|
23
|
+
|
|
24
|
+
_ACCEPT = 1
|
|
25
|
+
_REJECT = -1
|
|
26
|
+
_UNKNOWN = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class _MergeState:
|
|
31
|
+
"""Union-find over augmented-PTA nodes with per-block labels and transitions."""
|
|
32
|
+
|
|
33
|
+
parent: list[int]
|
|
34
|
+
label: list[int]
|
|
35
|
+
trans: list[dict[Any, int]]
|
|
36
|
+
|
|
37
|
+
def clone(self) -> _MergeState:
|
|
38
|
+
return _MergeState(
|
|
39
|
+
parent=self.parent.copy(),
|
|
40
|
+
label=self.label.copy(),
|
|
41
|
+
trans=[dict(row) for row in self.trans],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def find(self, node: int) -> int:
|
|
45
|
+
root = node
|
|
46
|
+
while self.parent[root] != root:
|
|
47
|
+
root = self.parent[root]
|
|
48
|
+
while self.parent[node] != root:
|
|
49
|
+
self.parent[node], node = root, self.parent[node]
|
|
50
|
+
return root
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _build_apta(
|
|
54
|
+
positive: Sequence[Sequence[Any]],
|
|
55
|
+
negative: Sequence[Sequence[Any]],
|
|
56
|
+
) -> tuple[list[int], list[dict[Any, int]]]:
|
|
57
|
+
"""Build an augmented PTA: labels are accept (+1), reject (-1), or unknown (0)."""
|
|
58
|
+
label: list[int] = [_UNKNOWN]
|
|
59
|
+
trans: list[dict[Any, int]] = [{}]
|
|
60
|
+
|
|
61
|
+
def ensure(node: int, symbol: Any) -> int:
|
|
62
|
+
if symbol not in trans[node]:
|
|
63
|
+
trans[node][symbol] = len(label)
|
|
64
|
+
label.append(_UNKNOWN)
|
|
65
|
+
trans.append({})
|
|
66
|
+
return trans[node][symbol]
|
|
67
|
+
|
|
68
|
+
for word, word_label in [(w, _ACCEPT) for w in positive] + [(w, _REJECT) for w in negative]:
|
|
69
|
+
node = 0
|
|
70
|
+
for symbol in word:
|
|
71
|
+
node = ensure(node, symbol)
|
|
72
|
+
if label[node] != _UNKNOWN and label[node] != word_label:
|
|
73
|
+
raise ValueError(f"contradictory labels for word {tuple(word)!r}")
|
|
74
|
+
label[node] = word_label
|
|
75
|
+
|
|
76
|
+
return label, trans
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _collect_alphabet(
|
|
80
|
+
positive: Sequence[Sequence[Any]],
|
|
81
|
+
negative: Sequence[Sequence[Any]],
|
|
82
|
+
) -> tuple[Any, ...]:
|
|
83
|
+
symbols: set[Any] = set()
|
|
84
|
+
for word in positive:
|
|
85
|
+
symbols.update(word)
|
|
86
|
+
for word in negative:
|
|
87
|
+
symbols.update(word)
|
|
88
|
+
return tuple(sorted(symbols, key=repr))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fold(state: _MergeState, red: int, blue: int) -> int | None:
|
|
92
|
+
"""Merge ``blue`` into ``red`` with deterministic folding.
|
|
93
|
+
|
|
94
|
+
Returns the evidence score (number of identically-labeled pairs folded) or
|
|
95
|
+
``None`` when a label conflict makes the merge inconsistent. Mutates
|
|
96
|
+
``state`` (call on a clone to evaluate a candidate).
|
|
97
|
+
"""
|
|
98
|
+
stack: list[tuple[int, int]] = [(red, blue)]
|
|
99
|
+
score = 0
|
|
100
|
+
while stack:
|
|
101
|
+
left, right = stack.pop()
|
|
102
|
+
x = state.find(left)
|
|
103
|
+
y = state.find(right)
|
|
104
|
+
if x == y:
|
|
105
|
+
continue
|
|
106
|
+
label_x, label_y = state.label[x], state.label[y]
|
|
107
|
+
if label_x != _UNKNOWN and label_y != _UNKNOWN:
|
|
108
|
+
if label_x != label_y:
|
|
109
|
+
return None
|
|
110
|
+
score += 1
|
|
111
|
+
# Determinism forces children on shared symbols to merge too.
|
|
112
|
+
for symbol, child in state.trans[y].items():
|
|
113
|
+
if symbol in state.trans[x]:
|
|
114
|
+
stack.append((state.trans[x][symbol], child))
|
|
115
|
+
state.parent[y] = x
|
|
116
|
+
if state.label[x] == _UNKNOWN:
|
|
117
|
+
state.label[x] = label_y
|
|
118
|
+
for symbol, child in state.trans[y].items():
|
|
119
|
+
state.trans[x].setdefault(symbol, child)
|
|
120
|
+
state.trans[y] = {}
|
|
121
|
+
return score
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _blue_fringe(state: _MergeState, red: list[int]) -> list[int]:
|
|
125
|
+
"""Return fringe (blue) representatives in canonical (red, symbol) order."""
|
|
126
|
+
red_reps = {state.find(r) for r in red}
|
|
127
|
+
fringe: list[int] = []
|
|
128
|
+
seen: set[int] = set()
|
|
129
|
+
for r in red:
|
|
130
|
+
rep = state.find(r)
|
|
131
|
+
for symbol in sorted(state.trans[rep], key=repr):
|
|
132
|
+
child = state.find(state.trans[rep][symbol])
|
|
133
|
+
if child not in red_reps and child not in seen:
|
|
134
|
+
seen.add(child)
|
|
135
|
+
fringe.append(child)
|
|
136
|
+
return fringe
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _to_dfa(state: _MergeState, red: list[int], alphabet: Sequence[Any]) -> DFA:
|
|
140
|
+
reps = sorted({state.find(r) for r in red})
|
|
141
|
+
rep_to_label: dict[int, Hashable] = {rep: f"q{index}" for index, rep in enumerate(reps)}
|
|
142
|
+
|
|
143
|
+
dfa = DFA(input_alphabet=frozenset(alphabet))
|
|
144
|
+
for name in rep_to_label.values():
|
|
145
|
+
dfa.graph.add_state(name)
|
|
146
|
+
dfa.initial_states = frozenset({rep_to_label[state.find(0)]})
|
|
147
|
+
|
|
148
|
+
accepting: set[Hashable] = set()
|
|
149
|
+
for rep in reps:
|
|
150
|
+
name = rep_to_label[rep]
|
|
151
|
+
if state.label[rep] == _ACCEPT:
|
|
152
|
+
accepting.add(name)
|
|
153
|
+
for symbol in sorted(state.trans[rep], key=repr):
|
|
154
|
+
target = state.find(state.trans[rep][symbol])
|
|
155
|
+
dfa.add_transition(name, rep_to_label[target], symbol)
|
|
156
|
+
dfa.accepting_states = frozenset(accepting)
|
|
157
|
+
dfa.validate()
|
|
158
|
+
return dfa
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def learn_dfa_edsm(
|
|
162
|
+
positive: Sequence[Sequence[Any]],
|
|
163
|
+
negative: Sequence[Sequence[Any]] | None = None,
|
|
164
|
+
) -> DFA:
|
|
165
|
+
"""Learn a DFA from labeled samples by evidence-driven state merging.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
positive
|
|
170
|
+
Words that must be accepted.
|
|
171
|
+
negative
|
|
172
|
+
Words that must be rejected.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
DFA
|
|
177
|
+
A deterministic automaton consistent with the sample, built with the
|
|
178
|
+
blue-fringe EDSM heuristic :cite:`Lang1998`.
|
|
179
|
+
"""
|
|
180
|
+
pos = [tuple(word) for word in positive]
|
|
181
|
+
neg = [tuple(word) for word in (negative or ())]
|
|
182
|
+
if not pos and not neg:
|
|
183
|
+
raise ValueError("at least one positive or negative sample is required")
|
|
184
|
+
|
|
185
|
+
alphabet = _collect_alphabet(pos, neg)
|
|
186
|
+
label, trans = _build_apta(pos, neg)
|
|
187
|
+
state = _MergeState(parent=list(range(len(label))), label=label, trans=trans)
|
|
188
|
+
|
|
189
|
+
red: list[int] = [0]
|
|
190
|
+
while True:
|
|
191
|
+
fringe = _blue_fringe(state, red)
|
|
192
|
+
if not fringe:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
scored: list[tuple[int, _MergeState]] = []
|
|
196
|
+
promote: int | None = None
|
|
197
|
+
for blue in fringe:
|
|
198
|
+
candidates: list[tuple[int, _MergeState]] = []
|
|
199
|
+
for r in red:
|
|
200
|
+
rep = state.find(r)
|
|
201
|
+
if rep == blue:
|
|
202
|
+
continue
|
|
203
|
+
trial = state.clone()
|
|
204
|
+
evidence = _fold(trial, rep, blue)
|
|
205
|
+
if evidence is not None:
|
|
206
|
+
candidates.append((evidence, trial))
|
|
207
|
+
if not candidates:
|
|
208
|
+
promote = blue
|
|
209
|
+
break
|
|
210
|
+
scored.extend(candidates)
|
|
211
|
+
|
|
212
|
+
if promote is not None:
|
|
213
|
+
red.append(promote)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
best = max(scored, key=lambda item: item[0])
|
|
217
|
+
state = best[1]
|
|
218
|
+
|
|
219
|
+
return _to_dfa(state, red, alphabet)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Finite-word enumeration for automata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from itertools import product
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sofic.automata.base import LabeledAutomaton
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def words_of_length(automaton: LabeledAutomaton, length: int) -> Iterator[tuple[Any, ...]]:
|
|
13
|
+
"""Yield accepted words of exactly ``length`` symbols."""
|
|
14
|
+
if length < 0:
|
|
15
|
+
raise ValueError("length must be nonnegative")
|
|
16
|
+
alphabet = sorted(_effective_alphabet(automaton), key=repr)
|
|
17
|
+
if length == 0:
|
|
18
|
+
if automaton.recognizes(()):
|
|
19
|
+
yield ()
|
|
20
|
+
return
|
|
21
|
+
if not alphabet:
|
|
22
|
+
return
|
|
23
|
+
for word in product(alphabet, repeat=length):
|
|
24
|
+
if automaton.recognizes(word):
|
|
25
|
+
yield word
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def iter_language(
|
|
29
|
+
automaton: LabeledAutomaton,
|
|
30
|
+
max_length: int | None = None,
|
|
31
|
+
) -> Iterator[tuple[Any, ...]]:
|
|
32
|
+
"""Yield accepted words in nondecreasing length order."""
|
|
33
|
+
if max_length is not None and max_length < 0:
|
|
34
|
+
raise ValueError("max_length must be nonnegative")
|
|
35
|
+
length = 0
|
|
36
|
+
while max_length is None or length <= max_length:
|
|
37
|
+
yield from words_of_length(automaton, length)
|
|
38
|
+
length += 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _effective_alphabet(automaton: LabeledAutomaton) -> frozenset[Any]:
|
|
42
|
+
from sofic.automata.algorithms import _effective_alphabet as _shared
|
|
43
|
+
|
|
44
|
+
return _shared(automaton)
|
sofic/automata/icdfa.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Enumeration and canonical strings for initially-connected DFAs (ICDFAs).
|
|
2
|
+
|
|
3
|
+
Implements the string representation and exact generation algorithm of
|
|
4
|
+
Almeida, Moreira, and Reis (2007), *Enumeration and generation with a string
|
|
5
|
+
automata representation*, Theoretical Computer Science 387(2):93--102.
|
|
6
|
+
DOI: 10.1016/j.tcs.2007.07.029.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import deque
|
|
12
|
+
from collections.abc import Hashable, Iterator, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from math import comb
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from sofic.automata.dfa import DFA
|
|
18
|
+
from sofic.exceptions import SoficValidationError
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ICDFAString",
|
|
22
|
+
"count_flag_sequences",
|
|
23
|
+
"count_icdfa",
|
|
24
|
+
"count_icdfa_empty",
|
|
25
|
+
"dfa_to_icdfa_string",
|
|
26
|
+
"first_icdfa_empty_string",
|
|
27
|
+
"flags_from_string",
|
|
28
|
+
"icdfa_string_to_dfa",
|
|
29
|
+
"iter_icdfa",
|
|
30
|
+
"iter_icdfa_empty_strings",
|
|
31
|
+
"last_icdfa_empty_string",
|
|
32
|
+
"next_flags",
|
|
33
|
+
"next_icdfa_empty_string",
|
|
34
|
+
"string_from_flags",
|
|
35
|
+
"validate_icdfa_empty_string",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ICDFAEnumerationError(SoficValidationError):
|
|
40
|
+
"""Raised when ICDFA enumeration or string conversion fails."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class ICDFAString:
|
|
45
|
+
"""Canonical transition string for an ICDFA structure (no final states)."""
|
|
46
|
+
|
|
47
|
+
transitions: tuple[int, ...]
|
|
48
|
+
n: int
|
|
49
|
+
k: int
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if self.n < 1 or self.k < 1:
|
|
53
|
+
raise ValueError("n and k must be positive")
|
|
54
|
+
if len(self.transitions) != self.k * self.n:
|
|
55
|
+
raise ValueError(f"expected {self.k * self.n} transitions, got {len(self.transitions)}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_icdfa_empty_string(
|
|
59
|
+
transitions: Sequence[int],
|
|
60
|
+
*,
|
|
61
|
+
n: int,
|
|
62
|
+
k: int,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Validate that ``transitions`` satisfies rules R1 and R2."""
|
|
65
|
+
length = k * n
|
|
66
|
+
if len(transitions) != length:
|
|
67
|
+
raise ICDFAEnumerationError(f"expected length {length}, got {len(transitions)}")
|
|
68
|
+
for value in transitions:
|
|
69
|
+
if not 0 <= value < n:
|
|
70
|
+
raise ICDFAEnumerationError(f"transition value {value!r} not in [0, {n - 1}]")
|
|
71
|
+
|
|
72
|
+
for state in range(2, n):
|
|
73
|
+
first_index = next(i for i, value in enumerate(transitions) if value == state)
|
|
74
|
+
if not any(transitions[j] == state - 1 for j in range(first_index)):
|
|
75
|
+
raise ICDFAEnumerationError(f"state {state} at index {first_index} appears before state {state - 1}")
|
|
76
|
+
|
|
77
|
+
for state in range(1, n):
|
|
78
|
+
if state not in transitions[: k * state]:
|
|
79
|
+
raise ICDFAEnumerationError(f"state {state} does not appear in the first {k * state} symbols")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def flags_from_string(transitions: Sequence[int], *, n: int) -> tuple[int, ...]:
|
|
83
|
+
"""Return first-occurrence indices ``(f_1, …, f_{n-1})`` for ``transitions``."""
|
|
84
|
+
if n <= 1:
|
|
85
|
+
return ()
|
|
86
|
+
flags: list[int] = []
|
|
87
|
+
for state in range(1, n):
|
|
88
|
+
try:
|
|
89
|
+
flags.append(transitions.index(state))
|
|
90
|
+
except ValueError as exc:
|
|
91
|
+
raise ICDFAEnumerationError(f"missing first occurrence of state {state}") from exc
|
|
92
|
+
return tuple(flags)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _validate_flags(flags: Sequence[int], *, n: int, k: int) -> None:
|
|
96
|
+
if len(flags) != n - 1:
|
|
97
|
+
raise ICDFAEnumerationError(f"expected {n - 1} flags, got {len(flags)}")
|
|
98
|
+
if not 0 <= flags[0] < k:
|
|
99
|
+
raise ICDFAEnumerationError(f"f_1={flags[0]} not in [0, {k - 1}]")
|
|
100
|
+
for index in range(1, len(flags)):
|
|
101
|
+
lower = flags[index - 1]
|
|
102
|
+
upper = k * (index + 1) - 1
|
|
103
|
+
if not lower < flags[index] <= upper:
|
|
104
|
+
raise ICDFAEnumerationError(f"flag f_{index + 1}={flags[index]} not in ({lower}, {upper}]")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def string_from_flags(
|
|
108
|
+
flags: Sequence[int],
|
|
109
|
+
*,
|
|
110
|
+
n: int,
|
|
111
|
+
k: int,
|
|
112
|
+
filler: int = 0,
|
|
113
|
+
) -> tuple[int, ...]:
|
|
114
|
+
"""Build the first ICDFA∅ string for a valid flag sequence."""
|
|
115
|
+
_validate_flags(flags, n=n, k=k)
|
|
116
|
+
transitions = [filler] * (k * n)
|
|
117
|
+
for state, flag in enumerate(flags, start=1):
|
|
118
|
+
transitions[flag] = state
|
|
119
|
+
return tuple(transitions)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def first_icdfa_empty_string(*, n: int, k: int) -> tuple[int, ...]:
|
|
123
|
+
"""Return the first ICDFA∅ string in generation order."""
|
|
124
|
+
if n == 1:
|
|
125
|
+
return (0,) * k
|
|
126
|
+
flags = tuple(k * state - 1 for state in range(1, n))
|
|
127
|
+
return string_from_flags(flags, n=n, k=k)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def last_icdfa_empty_string(*, n: int, k: int) -> tuple[int, ...]:
|
|
131
|
+
"""Return the last ICDFA∅ string in generation order."""
|
|
132
|
+
if n == 1:
|
|
133
|
+
return (0,) * k
|
|
134
|
+
flags = list(range(n - 1))
|
|
135
|
+
transitions = list(string_from_flags(flags, n=n, k=k))
|
|
136
|
+
flag_set = set(flags)
|
|
137
|
+
for index in range(k * n):
|
|
138
|
+
if index in flag_set:
|
|
139
|
+
continue
|
|
140
|
+
transitions[index] = _upper_bound_at(index, transitions, flags)
|
|
141
|
+
return tuple(transitions)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def next_flags(flags: list[int], *, k: int) -> None:
|
|
145
|
+
"""Advance ``flags`` in-place to the next valid flag sequence, or raise ``StopIteration``."""
|
|
146
|
+
|
|
147
|
+
def nextflags(index: int) -> None:
|
|
148
|
+
if index == 0:
|
|
149
|
+
if flags[0] == 0:
|
|
150
|
+
raise StopIteration
|
|
151
|
+
flags[0] -= 1
|
|
152
|
+
return
|
|
153
|
+
if flags[index] - 1 == flags[index - 1]:
|
|
154
|
+
flags[index] = k * (index + 1) - 1
|
|
155
|
+
nextflags(index - 1)
|
|
156
|
+
else:
|
|
157
|
+
flags[index] -= 1
|
|
158
|
+
|
|
159
|
+
nextflags(len(flags) - 1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _nearest_flag(flags: Sequence[int], index: int) -> tuple[int, int]:
|
|
163
|
+
label = 0
|
|
164
|
+
position = -1
|
|
165
|
+
for flag_index, flag_position in enumerate(flags):
|
|
166
|
+
if flag_position <= index and flag_position >= position:
|
|
167
|
+
label = flag_index + 1
|
|
168
|
+
position = flag_position
|
|
169
|
+
return label, position
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _upper_bound_at(index: int, transitions: Sequence[int], flags: Sequence[int]) -> int:
|
|
173
|
+
"""Return the maximum value allowed at ``index`` (non-flag positions only)."""
|
|
174
|
+
if index < flags[0]:
|
|
175
|
+
return 0
|
|
176
|
+
_, flag_position = _nearest_flag(flags, index)
|
|
177
|
+
return transitions[flag_position]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _is_last_icdfa_empty_string(
|
|
181
|
+
transitions: Sequence[int],
|
|
182
|
+
flags: Sequence[int],
|
|
183
|
+
*,
|
|
184
|
+
n: int,
|
|
185
|
+
) -> bool:
|
|
186
|
+
for index, value in enumerate(transitions):
|
|
187
|
+
if index in flags:
|
|
188
|
+
continue
|
|
189
|
+
if value < _upper_bound_at(index, transitions, flags):
|
|
190
|
+
return False
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def next_icdfa_empty_string(
|
|
195
|
+
transitions: list[int],
|
|
196
|
+
flags: Sequence[int],
|
|
197
|
+
*,
|
|
198
|
+
n: int,
|
|
199
|
+
k: int,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Advance ``transitions`` in-place to the next string for fixed ``flags``."""
|
|
202
|
+
if _is_last_icdfa_empty_string(transitions, flags, n=n):
|
|
203
|
+
raise StopIteration
|
|
204
|
+
|
|
205
|
+
flag_set = set(flags)
|
|
206
|
+
|
|
207
|
+
def nexticdfa(state: int, symbol: int) -> None:
|
|
208
|
+
index = state * k + symbol
|
|
209
|
+
while index in flag_set:
|
|
210
|
+
for tail in range(index + 1, k * n):
|
|
211
|
+
if tail not in flag_set:
|
|
212
|
+
transitions[tail] = 0
|
|
213
|
+
symbol -= 1
|
|
214
|
+
if symbol < 0:
|
|
215
|
+
if state == 0:
|
|
216
|
+
raise ICDFAEnumerationError("cannot advance past first ICDFA string for flags")
|
|
217
|
+
nexticdfa(state - 1, k - 1)
|
|
218
|
+
return
|
|
219
|
+
index -= 1
|
|
220
|
+
|
|
221
|
+
upper = _upper_bound_at(index, transitions, flags)
|
|
222
|
+
if transitions[index] == upper:
|
|
223
|
+
transitions[index] = 0
|
|
224
|
+
if symbol == 0:
|
|
225
|
+
if state == 0:
|
|
226
|
+
raise ICDFAEnumerationError("cannot advance past first ICDFA string for flags")
|
|
227
|
+
nexticdfa(state - 1, k - 1)
|
|
228
|
+
else:
|
|
229
|
+
nexticdfa(state, symbol - 1)
|
|
230
|
+
else:
|
|
231
|
+
transitions[index] += 1
|
|
232
|
+
|
|
233
|
+
nexticdfa(n - 1, k - 1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def count_flag_sequences(k: int, n: int) -> int:
|
|
237
|
+
"""Return ``F_{k,n}``, the number of valid flag sequences (Fuss--Catalan)."""
|
|
238
|
+
if n <= 1:
|
|
239
|
+
return 1
|
|
240
|
+
return comb(k * n, n) // ((k - 1) * n + 1)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def count_icdfa_empty(k: int, n: int) -> int:
|
|
244
|
+
"""Return ``B_{k,n}``, the number of non-isomorphic ICDFA∅ structures."""
|
|
245
|
+
if n == 1:
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
total = 0
|
|
249
|
+
|
|
250
|
+
def visit(prefix: list[int]) -> None:
|
|
251
|
+
nonlocal total
|
|
252
|
+
depth = len(prefix)
|
|
253
|
+
if depth == n - 1:
|
|
254
|
+
product = 1
|
|
255
|
+
previous = -1
|
|
256
|
+
extended = prefix + [k * n]
|
|
257
|
+
for label in range(1, n + 1):
|
|
258
|
+
current = extended[label - 1]
|
|
259
|
+
product *= label ** (current - previous - 1)
|
|
260
|
+
previous = current
|
|
261
|
+
total += product
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
lower = 0 if depth == 0 else prefix[-1] + 1
|
|
265
|
+
upper = k * (depth + 1)
|
|
266
|
+
for value in range(lower, upper):
|
|
267
|
+
prefix.append(value)
|
|
268
|
+
visit(prefix)
|
|
269
|
+
prefix.pop()
|
|
270
|
+
|
|
271
|
+
visit([])
|
|
272
|
+
return total
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def count_icdfa(k: int, n: int) -> int:
|
|
276
|
+
"""Return the number of non-isomorphic ICDFAs (with final states)."""
|
|
277
|
+
return (2**n) * count_icdfa_empty(k, n)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def iter_icdfa_empty_strings(k: int, n: int) -> Iterator[tuple[int, ...]]:
|
|
281
|
+
"""Yield all ICDFA∅ transition strings in paper generation order."""
|
|
282
|
+
if n == 1:
|
|
283
|
+
yield (0,) * k
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
flags = [k * state - 1 for state in range(1, n)]
|
|
287
|
+
transitions = list(string_from_flags(flags, n=n, k=k))
|
|
288
|
+
while True:
|
|
289
|
+
yield tuple(transitions)
|
|
290
|
+
try:
|
|
291
|
+
next_icdfa_empty_string(transitions, flags, n=n, k=k)
|
|
292
|
+
except StopIteration:
|
|
293
|
+
try:
|
|
294
|
+
next_flags(flags, k=k)
|
|
295
|
+
except StopIteration:
|
|
296
|
+
break
|
|
297
|
+
transitions[:] = list(string_from_flags(flags, n=n, k=k))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def iter_icdfa(k: int, n: int) -> Iterator[tuple[tuple[int, ...], frozenset[int]]]:
|
|
301
|
+
"""Yield ``(transitions, final_states)`` for all ICDFAs with ``n`` states and alphabet size ``k``."""
|
|
302
|
+
for transitions in iter_icdfa_empty_strings(k, n):
|
|
303
|
+
for mask in range(2**n):
|
|
304
|
+
finals = frozenset(state for state in range(n) if (mask >> state) & 1)
|
|
305
|
+
yield transitions, finals
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _ordered_alphabet(
|
|
309
|
+
alphabet: Sequence[Any],
|
|
310
|
+
*,
|
|
311
|
+
symbol_order: Sequence[Any] | None,
|
|
312
|
+
) -> tuple[Any, ...]:
|
|
313
|
+
if symbol_order is not None:
|
|
314
|
+
order = tuple(symbol_order)
|
|
315
|
+
if len(order) != len(alphabet) or len(set(order)) != len(order):
|
|
316
|
+
raise ICDFAEnumerationError("symbol_order must be a permutation of alphabet")
|
|
317
|
+
if set(order) != set(alphabet):
|
|
318
|
+
raise ICDFAEnumerationError("symbol_order must match alphabet")
|
|
319
|
+
return order
|
|
320
|
+
return tuple(sorted(alphabet, key=lambda value: (type(value).__name__, value)))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def icdfa_string_to_dfa(
|
|
324
|
+
transitions: Sequence[int],
|
|
325
|
+
alphabet: Sequence[Any],
|
|
326
|
+
*,
|
|
327
|
+
n: int | None = None,
|
|
328
|
+
k: int | None = None,
|
|
329
|
+
final_states: frozenset[int] | None = None,
|
|
330
|
+
symbol_order: Sequence[Any] | None = None,
|
|
331
|
+
state_labels: Sequence[Hashable] | None = None,
|
|
332
|
+
) -> DFA:
|
|
333
|
+
"""Decode a canonical ICDFA string into a complete :class:`DFA`."""
|
|
334
|
+
symbols = _ordered_alphabet(alphabet, symbol_order=symbol_order)
|
|
335
|
+
inferred_k = len(symbols)
|
|
336
|
+
inferred_n = len(transitions) // inferred_k if inferred_k else 0
|
|
337
|
+
states_count = n if n is not None else inferred_n
|
|
338
|
+
alphabet_size = k if k is not None else inferred_k
|
|
339
|
+
if states_count * alphabet_size != len(transitions):
|
|
340
|
+
raise ICDFAEnumerationError("transitions length does not match n and k")
|
|
341
|
+
|
|
342
|
+
validate_icdfa_empty_string(transitions, n=states_count, k=alphabet_size)
|
|
343
|
+
|
|
344
|
+
if state_labels is None:
|
|
345
|
+
labels: tuple[Hashable, ...] = tuple(range(states_count))
|
|
346
|
+
else:
|
|
347
|
+
labels = tuple(state_labels)
|
|
348
|
+
if len(labels) != states_count:
|
|
349
|
+
raise ICDFAEnumerationError("state_labels length must equal n")
|
|
350
|
+
|
|
351
|
+
acceptors = final_states if final_states is not None else frozenset()
|
|
352
|
+
dfa = DFA(
|
|
353
|
+
input_alphabet=frozenset(symbols),
|
|
354
|
+
initial_states=frozenset({labels[0]}),
|
|
355
|
+
accepting_states=frozenset(labels[state] for state in acceptors),
|
|
356
|
+
)
|
|
357
|
+
for label in labels:
|
|
358
|
+
dfa.graph.add_state(label)
|
|
359
|
+
for index, target in enumerate(transitions):
|
|
360
|
+
source = labels[index // alphabet_size]
|
|
361
|
+
symbol = symbols[index % alphabet_size]
|
|
362
|
+
dfa.add_transition(source, labels[target], symbol)
|
|
363
|
+
dfa.validate()
|
|
364
|
+
return dfa
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def dfa_to_icdfa_string(
|
|
368
|
+
dfa: DFA,
|
|
369
|
+
*,
|
|
370
|
+
symbol_order: Sequence[Any] | None = None,
|
|
371
|
+
) -> ICDFAString:
|
|
372
|
+
"""Encode a complete initially-connected DFA as a canonical ICDFA string."""
|
|
373
|
+
from sofic.automata.algorithms import _forward_reachable
|
|
374
|
+
|
|
375
|
+
reachable = _forward_reachable(dfa)
|
|
376
|
+
if reachable != set(dfa.states()):
|
|
377
|
+
raise ICDFAEnumerationError("DFA must be initially connected (all states reachable)")
|
|
378
|
+
|
|
379
|
+
symbols = _ordered_alphabet(tuple(dfa.input_alphabet), symbol_order=symbol_order)
|
|
380
|
+
k = len(symbols)
|
|
381
|
+
if k == 0:
|
|
382
|
+
raise ICDFAEnumerationError("DFA must have a non-empty input alphabet")
|
|
383
|
+
|
|
384
|
+
if len(dfa.initial_states) != 1:
|
|
385
|
+
raise ICDFAEnumerationError("DFA must have exactly one initial state")
|
|
386
|
+
initial = next(iter(dfa.initial_states))
|
|
387
|
+
|
|
388
|
+
for state in dfa.states():
|
|
389
|
+
for symbol in symbols:
|
|
390
|
+
if len(dfa.delta(state, symbol)) != 1:
|
|
391
|
+
raise ICDFAEnumerationError("DFA must be complete")
|
|
392
|
+
|
|
393
|
+
state_to_index: dict[Hashable, int] = {}
|
|
394
|
+
index_to_state: list[Hashable] = []
|
|
395
|
+
queue: deque[Hashable] = deque([initial])
|
|
396
|
+
state_to_index[initial] = 0
|
|
397
|
+
index_to_state.append(initial)
|
|
398
|
+
|
|
399
|
+
while queue:
|
|
400
|
+
current = queue.popleft()
|
|
401
|
+
for symbol in symbols:
|
|
402
|
+
successors = dfa.delta(current, symbol)
|
|
403
|
+
target = next(iter(successors))
|
|
404
|
+
if target not in state_to_index:
|
|
405
|
+
state_to_index[target] = len(index_to_state)
|
|
406
|
+
index_to_state.append(target)
|
|
407
|
+
queue.append(target)
|
|
408
|
+
|
|
409
|
+
if set(index_to_state) != reachable:
|
|
410
|
+
raise ICDFAEnumerationError("DFA must be initially connected")
|
|
411
|
+
|
|
412
|
+
n = len(index_to_state)
|
|
413
|
+
transitions: list[int] = []
|
|
414
|
+
for index in range(n):
|
|
415
|
+
state = index_to_state[index]
|
|
416
|
+
for symbol in symbols:
|
|
417
|
+
target = next(iter(dfa.delta(state, symbol)))
|
|
418
|
+
transitions.append(state_to_index[target])
|
|
419
|
+
|
|
420
|
+
validate_icdfa_empty_string(transitions, n=n, k=k)
|
|
421
|
+
return ICDFAString(transitions=tuple(transitions), n=n, k=k)
|