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.
Files changed (150) hide show
  1. sofic/__init__.py +185 -0
  2. sofic/automata/__init__.py +207 -0
  3. sofic/automata/_config_simulation.py +40 -0
  4. sofic/automata/active.py +611 -0
  5. sofic/automata/alergia.py +222 -0
  6. sofic/automata/algorithms.py +376 -0
  7. sofic/automata/atomaton.py +58 -0
  8. sofic/automata/base.py +161 -0
  9. sofic/automata/buchi.py +23 -0
  10. sofic/automata/buchi_simulation.py +67 -0
  11. sofic/automata/canonical_dual.py +18 -0
  12. sofic/automata/canonical_extraction.py +122 -0
  13. sofic/automata/dfa.py +85 -0
  14. sofic/automata/dfasat.py +195 -0
  15. sofic/automata/edsm.py +219 -0
  16. sofic/automata/enumeration.py +44 -0
  17. sofic/automata/icdfa.py +421 -0
  18. sofic/automata/idfa.py +363 -0
  19. sofic/automata/languages/__init__.py +39 -0
  20. sofic/automata/languages/_quotient_utils.py +64 -0
  21. sofic/automata/languages/atoms.py +31 -0
  22. sofic/automata/languages/automaton_ops.py +243 -0
  23. sofic/automata/languages/base.py +67 -0
  24. sofic/automata/languages/operations.py +78 -0
  25. sofic/automata/languages/quotients.py +66 -0
  26. sofic/automata/languages/residuals.py +25 -0
  27. sofic/automata/learning.py +79 -0
  28. sofic/automata/nfa.py +39 -0
  29. sofic/automata/nwa.py +343 -0
  30. sofic/automata/nwa_simulation.py +56 -0
  31. sofic/automata/observation.py +40 -0
  32. sofic/automata/papni.py +301 -0
  33. sofic/automata/regex.py +128 -0
  34. sofic/automata/rfsa.py +35 -0
  35. sofic/automata/rpni.py +193 -0
  36. sofic/automata/subsequential.py +201 -0
  37. sofic/automata/transducer_operations.py +350 -0
  38. sofic/automata/transducer_simulation.py +150 -0
  39. sofic/automata/transducers.py +365 -0
  40. sofic/automata/unifilar.py +107 -0
  41. sofic/automata/vpa.py +1373 -0
  42. sofic/automata/vpa_simulation.py +53 -0
  43. sofic/base.py +153 -0
  44. sofic/core.py +47 -0
  45. sofic/examples/__init__.py +86 -0
  46. sofic/examples/epsilon_machines.py +1089 -0
  47. sofic/examples/processes.py +1491 -0
  48. sofic/examples/shifts.py +144 -0
  49. sofic/exceptions.py +33 -0
  50. sofic/generators/__init__.py +115 -0
  51. sofic/generators/_word_measures.py +94 -0
  52. sofic/generators/alternative_complexity.py +104 -0
  53. sofic/generators/base.py +327 -0
  54. sofic/generators/bidirectional_construction.py +717 -0
  55. sofic/generators/bidirectional_epsilon_machine.py +689 -0
  56. sofic/generators/block_convergence.py +668 -0
  57. sofic/generators/block_entropy.py +578 -0
  58. sofic/generators/channel_measures.py +75 -0
  59. sofic/generators/conversions.py +182 -0
  60. sofic/generators/directional_flow.py +245 -0
  61. sofic/generators/edge_emissions.py +36 -0
  62. sofic/generators/edge_machine.py +178 -0
  63. sofic/generators/epsilon_construction.py +193 -0
  64. sofic/generators/epsilon_inference.py +703 -0
  65. sofic/generators/epsilon_machine.py +557 -0
  66. sofic/generators/epsilon_transducer.py +168 -0
  67. sofic/generators/epsilon_transducer_construction.py +185 -0
  68. sofic/generators/epsilon_transducer_inference.py +499 -0
  69. sofic/generators/hmm_inference.py +719 -0
  70. sofic/generators/information_diagram.py +428 -0
  71. sofic/generators/lumping.py +447 -0
  72. sofic/generators/markov.py +100 -0
  73. sofic/generators/mealy.py +156 -0
  74. sofic/generators/measures.py +257 -0
  75. sofic/generators/minimal_generative_model.py +821 -0
  76. sofic/generators/mixed_state.py +250 -0
  77. sofic/generators/mixed_state_construction.py +163 -0
  78. sofic/generators/moore.py +75 -0
  79. sofic/generators/nmachine.py +78 -0
  80. sofic/generators/nmachine_construction.py +70 -0
  81. sofic/generators/pfa.py +100 -0
  82. sofic/generators/prob.py +291 -0
  83. sofic/generators/process_equivalence.py +207 -0
  84. sofic/generators/quasi_inference.py +74 -0
  85. sofic/generators/quasi_realization.py +97 -0
  86. sofic/generators/reversal.py +66 -0
  87. sofic/generators/stack_hmm.py +426 -0
  88. sofic/generators/stack_inference.py +509 -0
  89. sofic/generators/stationary.py +134 -0
  90. sofic/generators/stochastic.py +65 -0
  91. sofic/generators/synchronization.py +407 -0
  92. sofic/generators/topological_epsilon_enumeration.py +349 -0
  93. sofic/generators/words.py +226 -0
  94. sofic/graph.py +135 -0
  95. sofic/indexing.py +31 -0
  96. sofic/inference/__init__.py +45 -0
  97. sofic/inference/bayesian/__init__.py +68 -0
  98. sofic/inference/bayesian/comparison.py +199 -0
  99. sofic/inference/bayesian/counts.py +219 -0
  100. sofic/inference/bayesian/diversity.py +254 -0
  101. sofic/inference/bayesian/epsilon.py +270 -0
  102. sofic/inference/bayesian/hdp_hmm.py +340 -0
  103. sofic/inference/bayesian/markov.py +294 -0
  104. sofic/inference/bayesian/pymc_backend.py +71 -0
  105. sofic/inference/bayesian/stack_hmm.py +215 -0
  106. sofic/inference/model_selection.py +365 -0
  107. sofic/inference/spectral.py +564 -0
  108. sofic/operations.py +16 -0
  109. sofic/properties.py +339 -0
  110. sofic/serialization.py +450 -0
  111. sofic/shifts/__init__.py +48 -0
  112. sofic/shifts/algorithms.py +84 -0
  113. sofic/shifts/base.py +49 -0
  114. sofic/shifts/cover_construction.py +76 -0
  115. sofic/shifts/covers.py +47 -0
  116. sofic/shifts/dyck_algorithms.py +100 -0
  117. sofic/shifts/dyck_enumeration.py +275 -0
  118. sofic/shifts/markov_dyck.py +172 -0
  119. sofic/shifts/parry_construction.py +82 -0
  120. sofic/shifts/sft.py +104 -0
  121. sofic/shifts/sft_construction.py +52 -0
  122. sofic/shifts/sliding_block_code.py +156 -0
  123. sofic/shifts/sofic.py +111 -0
  124. sofic/shifts/sofic_dyck.py +110 -0
  125. sofic/shifts/sofic_relation.py +64 -0
  126. sofic/shifts/textile.py +104 -0
  127. sofic/shifts/tmc.py +46 -0
  128. sofic/shifts/tmc_construction.py +58 -0
  129. sofic/shifts/topological_anatomy.py +150 -0
  130. sofic/states.py +27 -0
  131. sofic/testing/__init__.py +8 -0
  132. sofic/testing/strategies.py +154 -0
  133. sofic/viz/__init__.py +16 -0
  134. sofic/viz/_context.py +345 -0
  135. sofic/viz/_edge.py +216 -0
  136. sofic/viz/_format.py +89 -0
  137. sofic/viz/_labels.py +34 -0
  138. sofic/viz/_names.py +17 -0
  139. sofic/viz/_rational.py +20 -0
  140. sofic/viz/_tikz_compile.py +177 -0
  141. sofic/viz/_tikz_format.py +122 -0
  142. sofic/viz/_tikz_layout.py +218 -0
  143. sofic/viz/assets/vaucanson.tikz +71 -0
  144. sofic/viz/graphviz.py +158 -0
  145. sofic/viz/idiagram.py +350 -0
  146. sofic/viz/tikz.py +381 -0
  147. sofic-0.1.0.dist-info/METADATA +444 -0
  148. sofic-0.1.0.dist-info/RECORD +150 -0
  149. sofic-0.1.0.dist-info/WHEEL +4 -0
  150. 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)
@@ -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)