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/idfa.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Enumeration of incomplete accessible DFA structures (IDFA∅).
|
|
2
|
+
|
|
3
|
+
Extends the complete ICDFA string representation of Almeida, Moreira, and Reis
|
|
4
|
+
(2007) with ``-1`` for missing transitions, following the accessible-DFA
|
|
5
|
+
generation and rank function ``B¹_{n,k}`` used by Johnson et al. (2010),
|
|
6
|
+
*Enumerating Finitary Processes* (arXiv:1011.0036).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterator, Sequence
|
|
12
|
+
from functools import cache
|
|
13
|
+
|
|
14
|
+
from sofic.automata.icdfa import (
|
|
15
|
+
_upper_bound_at,
|
|
16
|
+
_validate_flags,
|
|
17
|
+
next_flags,
|
|
18
|
+
)
|
|
19
|
+
from sofic.exceptions import SoficValidationError
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"MISSING_TRANSITION",
|
|
23
|
+
"count_accessible_idfa",
|
|
24
|
+
"count_idfa_strings_for_flags",
|
|
25
|
+
"extended_flags",
|
|
26
|
+
"first_idfa_string",
|
|
27
|
+
"idfa_string_to_topological_graph",
|
|
28
|
+
"iter_idfa_strings",
|
|
29
|
+
"last_idfa_string",
|
|
30
|
+
"next_idfa_string",
|
|
31
|
+
"rank_idfa_string",
|
|
32
|
+
"transition_count",
|
|
33
|
+
"unrank_idfa_string",
|
|
34
|
+
"validate_idfa_string",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
MISSING_TRANSITION = -1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IDFAEnumerationError(SoficValidationError):
|
|
41
|
+
"""Raised when incomplete accessible DFA enumeration fails."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extended_flags(flags: Sequence[int], *, n: int, k: int) -> tuple[int, ...]:
|
|
45
|
+
"""Return ``(f_0, …, f_n)`` with ``f_0 = -1`` and ``f_n = nk``."""
|
|
46
|
+
return (MISSING_TRANSITION,) + tuple(flags) + (n * k,)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_idfa_string(
|
|
50
|
+
transitions: Sequence[int],
|
|
51
|
+
*,
|
|
52
|
+
n: int,
|
|
53
|
+
k: int,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Validate an incomplete accessible DFA string (rules R1 and R2)."""
|
|
56
|
+
length = k * n
|
|
57
|
+
if len(transitions) != length:
|
|
58
|
+
raise IDFAEnumerationError(f"expected length {length}, got {len(transitions)}")
|
|
59
|
+
for value in transitions:
|
|
60
|
+
if value != MISSING_TRANSITION and not 0 <= value < n:
|
|
61
|
+
raise IDFAEnumerationError(f"transition value {value!r} not in [-1, {n - 1}]")
|
|
62
|
+
|
|
63
|
+
for index, value in enumerate(transitions):
|
|
64
|
+
if value == MISSING_TRANSITION or value <= 1:
|
|
65
|
+
continue
|
|
66
|
+
if not any(transitions[j] == value - 1 for j in range(index) if transitions[j] != MISSING_TRANSITION):
|
|
67
|
+
raise IDFAEnumerationError(f"state {value} at index {index} appears before state {value - 1}")
|
|
68
|
+
|
|
69
|
+
for state in range(1, n):
|
|
70
|
+
if state not in transitions[: k * state]:
|
|
71
|
+
raise IDFAEnumerationError(f"state {state} does not appear in the first {k * state} symbols")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def transition_count(transitions: Sequence[int]) -> int:
|
|
75
|
+
"""Return the number of defined transitions in ``transitions``."""
|
|
76
|
+
return sum(1 for value in transitions if value != MISSING_TRANSITION)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def string_from_flags(
|
|
80
|
+
flags: Sequence[int],
|
|
81
|
+
*,
|
|
82
|
+
n: int,
|
|
83
|
+
k: int,
|
|
84
|
+
filler: int = MISSING_TRANSITION,
|
|
85
|
+
) -> tuple[int, ...]:
|
|
86
|
+
"""Build the first IDFA∅ string for a valid flag sequence."""
|
|
87
|
+
_validate_flags(flags, n=n, k=k)
|
|
88
|
+
transitions = [filler] * (k * n)
|
|
89
|
+
for state, flag in enumerate(flags, start=1):
|
|
90
|
+
transitions[flag] = state
|
|
91
|
+
for index in range(flags[0]):
|
|
92
|
+
transitions[index] = 0
|
|
93
|
+
return tuple(transitions)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def first_idfa_string(*, n: int, k: int) -> tuple[int, ...]:
|
|
97
|
+
"""Return the first incomplete accessible DFA string in generation order."""
|
|
98
|
+
if n == 1:
|
|
99
|
+
return (MISSING_TRANSITION,) * k
|
|
100
|
+
flags = tuple(k * state - 1 for state in range(1, n))
|
|
101
|
+
return string_from_flags(flags, n=n, k=k)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def last_idfa_string(*, n: int, k: int) -> tuple[int, ...]:
|
|
105
|
+
"""Return the last incomplete accessible DFA string for the last flag sequence."""
|
|
106
|
+
if n == 1:
|
|
107
|
+
return (n - 1,) * k
|
|
108
|
+
flags = list(range(n - 1))
|
|
109
|
+
transitions = list(string_from_flags(flags, n=n, k=k))
|
|
110
|
+
flag_set = set(flags)
|
|
111
|
+
for index in range(k * n):
|
|
112
|
+
if index in flag_set:
|
|
113
|
+
continue
|
|
114
|
+
transitions[index] = _upper_bound_at(index, transitions, flags)
|
|
115
|
+
return tuple(transitions)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_last_idfa_string(
|
|
119
|
+
transitions: Sequence[int],
|
|
120
|
+
flags: Sequence[int],
|
|
121
|
+
*,
|
|
122
|
+
n: int,
|
|
123
|
+
) -> bool:
|
|
124
|
+
for index, value in enumerate(transitions):
|
|
125
|
+
if index in flags:
|
|
126
|
+
continue
|
|
127
|
+
if value < _upper_bound_at(index, transitions, flags):
|
|
128
|
+
return False
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _advance_idfa_value(current: int, upper: int) -> tuple[int, bool]:
|
|
133
|
+
"""Advance one position; return ``(new_value, carry)``."""
|
|
134
|
+
if current < upper:
|
|
135
|
+
if current == MISSING_TRANSITION and upper < 0:
|
|
136
|
+
return MISSING_TRANSITION, True
|
|
137
|
+
if current == MISSING_TRANSITION:
|
|
138
|
+
return 0, False
|
|
139
|
+
return current + 1, False
|
|
140
|
+
if current == upper:
|
|
141
|
+
return MISSING_TRANSITION, True
|
|
142
|
+
raise IDFAEnumerationError(f"invalid transition value {current!r} with upper bound {upper}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def next_idfa_string(
|
|
146
|
+
transitions: list[int],
|
|
147
|
+
flags: Sequence[int],
|
|
148
|
+
*,
|
|
149
|
+
n: int,
|
|
150
|
+
k: int,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Advance ``transitions`` in-place to the next string for fixed ``flags``."""
|
|
153
|
+
if _is_last_idfa_string(transitions, flags, n=n):
|
|
154
|
+
raise StopIteration
|
|
155
|
+
|
|
156
|
+
flag_set = set(flags)
|
|
157
|
+
|
|
158
|
+
def nextidfa(state: int, symbol: int) -> None:
|
|
159
|
+
index = state * k + symbol
|
|
160
|
+
while index in flag_set:
|
|
161
|
+
for tail in range(index + 1, k * n):
|
|
162
|
+
if tail not in flag_set:
|
|
163
|
+
transitions[tail] = MISSING_TRANSITION
|
|
164
|
+
symbol -= 1
|
|
165
|
+
if symbol < 0:
|
|
166
|
+
if state == 0:
|
|
167
|
+
raise IDFAEnumerationError("cannot advance past first IDFA string for flags")
|
|
168
|
+
nextidfa(state - 1, k - 1)
|
|
169
|
+
return
|
|
170
|
+
index -= 1
|
|
171
|
+
|
|
172
|
+
upper = _upper_bound_at(index, transitions, flags)
|
|
173
|
+
new_value, carry = _advance_idfa_value(transitions[index], upper)
|
|
174
|
+
transitions[index] = new_value
|
|
175
|
+
if carry:
|
|
176
|
+
if symbol == 0:
|
|
177
|
+
if state == 0:
|
|
178
|
+
raise IDFAEnumerationError("cannot advance past first IDFA string for flags")
|
|
179
|
+
nextidfa(state - 1, k - 1)
|
|
180
|
+
else:
|
|
181
|
+
nextidfa(state, symbol - 1)
|
|
182
|
+
|
|
183
|
+
nextidfa(n - 1, k - 1)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def count_idfa_strings_for_flags(flags: Sequence[int], *, n: int, k: int) -> int:
|
|
187
|
+
"""Return the number of IDFA∅ strings with the given flag sequence."""
|
|
188
|
+
ext = extended_flags(flags, n=n, k=k)
|
|
189
|
+
product = 1
|
|
190
|
+
for segment_index in range(n):
|
|
191
|
+
segment = ext[segment_index + 1] - ext[segment_index] - 1
|
|
192
|
+
if segment == 0:
|
|
193
|
+
continue
|
|
194
|
+
if segment_index == 0:
|
|
195
|
+
continue
|
|
196
|
+
product *= (segment_index + 2) ** segment
|
|
197
|
+
return product
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def count_accessible_idfa(k: int, n: int) -> int:
|
|
201
|
+
"""Return ``B¹_{k,n}``, the number of incomplete accessible DFA∅ structures."""
|
|
202
|
+
if n == 1:
|
|
203
|
+
return 2**k
|
|
204
|
+
|
|
205
|
+
total = 0
|
|
206
|
+
|
|
207
|
+
def visit(prefix: list[int]) -> None:
|
|
208
|
+
nonlocal total
|
|
209
|
+
depth = len(prefix)
|
|
210
|
+
if depth == n - 1:
|
|
211
|
+
total += count_idfa_strings_for_flags(prefix, n=n, k=k)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
lower = 0 if depth == 0 else prefix[-1] + 1
|
|
215
|
+
upper = k * (depth + 1)
|
|
216
|
+
for value in range(lower, upper):
|
|
217
|
+
prefix.append(value)
|
|
218
|
+
visit(prefix)
|
|
219
|
+
prefix.pop()
|
|
220
|
+
|
|
221
|
+
visit([])
|
|
222
|
+
return total
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@cache
|
|
226
|
+
def _enumeration_index(k: int, n: int) -> dict[tuple[int, ...], int]:
|
|
227
|
+
return {candidate: rank for rank, candidate in enumerate(_iter_idfa_strings_impl(k, n))}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def rank_idfa_string(transitions: Sequence[int], *, n: int, k: int) -> int:
|
|
231
|
+
"""Return ``B¹_{n,k}(S)``, the rank of ``transitions`` in generation order."""
|
|
232
|
+
validate_idfa_string(transitions, n=n, k=k)
|
|
233
|
+
try:
|
|
234
|
+
return _enumeration_index(k, n)[tuple(transitions)]
|
|
235
|
+
except KeyError as exc:
|
|
236
|
+
raise IDFAEnumerationError("string is not in the accessible DFA enumeration") from exc
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def unrank_idfa_string(rank: int, *, n: int, k: int) -> tuple[int, ...]:
|
|
240
|
+
"""Return the IDFA∅ string with rank ``rank`` in ``[0, B¹_{n,k})``."""
|
|
241
|
+
if rank < 0:
|
|
242
|
+
raise IDFAEnumerationError("rank must be nonnegative")
|
|
243
|
+
if n == 1:
|
|
244
|
+
if rank >= 2**k:
|
|
245
|
+
raise IDFAEnumerationError("rank out of range")
|
|
246
|
+
transitions: list[int] = []
|
|
247
|
+
for index in range(k):
|
|
248
|
+
mask = 2 ** (k - 1 - index)
|
|
249
|
+
transitions.append(0 if rank & mask else MISSING_TRANSITION)
|
|
250
|
+
return tuple(transitions)
|
|
251
|
+
for index, candidate in enumerate(_iter_idfa_strings_impl(k, n)):
|
|
252
|
+
if index == rank:
|
|
253
|
+
return candidate
|
|
254
|
+
raise IDFAEnumerationError("rank out of range")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _iter_idfa_strings_impl(k: int, n: int) -> Iterator[tuple[int, ...]]:
|
|
258
|
+
if n == 1:
|
|
259
|
+
for rank in range(2**k):
|
|
260
|
+
yield unrank_idfa_string(rank, n=n, k=k)
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
flags = [k * state - 1 for state in range(1, n)]
|
|
264
|
+
transitions = list(string_from_flags(flags, n=n, k=k))
|
|
265
|
+
while True:
|
|
266
|
+
yield tuple(transitions)
|
|
267
|
+
try:
|
|
268
|
+
next_idfa_string(transitions, flags, n=n, k=k)
|
|
269
|
+
except StopIteration:
|
|
270
|
+
try:
|
|
271
|
+
next_flags(flags, k=k)
|
|
272
|
+
except StopIteration:
|
|
273
|
+
break
|
|
274
|
+
transitions[:] = list(string_from_flags(flags, n=n, k=k))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def iter_idfa_strings(k: int, n: int) -> Iterator[tuple[int, ...]]:
|
|
278
|
+
"""Yield all incomplete accessible DFA∅ strings in generation order."""
|
|
279
|
+
yield from _iter_idfa_strings_impl(k, n)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def idfa_string_to_topological_graph(
|
|
283
|
+
transitions: Sequence[int],
|
|
284
|
+
*,
|
|
285
|
+
n: int,
|
|
286
|
+
k: int,
|
|
287
|
+
alphabet: Sequence[object] | None = None,
|
|
288
|
+
):
|
|
289
|
+
"""Decode an IDFA string into a :class:`~sofic.generators.synchronization.TopologicalUnifilarGraph`."""
|
|
290
|
+
from sofic.generators.synchronization import TopologicalUnifilarGraph
|
|
291
|
+
|
|
292
|
+
validate_idfa_string(transitions, n=n, k=k)
|
|
293
|
+
if alphabet is None:
|
|
294
|
+
symbols = tuple(range(k))
|
|
295
|
+
else:
|
|
296
|
+
if len(alphabet) != k:
|
|
297
|
+
raise IDFAEnumerationError("alphabet length must equal k")
|
|
298
|
+
symbols = tuple(alphabet)
|
|
299
|
+
|
|
300
|
+
states = frozenset(range(n))
|
|
301
|
+
edges: dict[tuple[int, object], int] = {}
|
|
302
|
+
for index, target in enumerate(transitions):
|
|
303
|
+
if target == MISSING_TRANSITION:
|
|
304
|
+
continue
|
|
305
|
+
source = index // k
|
|
306
|
+
symbol = symbols[index % k]
|
|
307
|
+
edges[(source, symbol)] = target
|
|
308
|
+
return TopologicalUnifilarGraph(states=states, alphabet=frozenset(symbols), transitions=edges)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _delta_table(transitions: Sequence[int], *, n: int, k: int) -> list[list[int | None]]:
|
|
312
|
+
table: list[list[int | None]] = [[None] * k for _ in range(n)]
|
|
313
|
+
for index, target in enumerate(transitions):
|
|
314
|
+
if target == MISSING_TRANSITION:
|
|
315
|
+
continue
|
|
316
|
+
table[index // k][index % k] = target
|
|
317
|
+
return table
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reroot_idfa_string(
|
|
321
|
+
transitions: Sequence[int],
|
|
322
|
+
*,
|
|
323
|
+
new_root: int,
|
|
324
|
+
n: int,
|
|
325
|
+
k: int,
|
|
326
|
+
) -> tuple[int, ...] | None:
|
|
327
|
+
"""Relabel with ``new_root`` as state ``0``; return ``None`` if labeling fails."""
|
|
328
|
+
validate_idfa_string(transitions, n=n, k=k)
|
|
329
|
+
table = _delta_table(transitions, n=n, k=k)
|
|
330
|
+
old_to_new: dict[int, int] = {new_root: 0}
|
|
331
|
+
new_to_old: list[int] = [new_root]
|
|
332
|
+
next_label = 1
|
|
333
|
+
|
|
334
|
+
while True:
|
|
335
|
+
progressed = False
|
|
336
|
+
for new_source in range(len(new_to_old)):
|
|
337
|
+
old_source = new_to_old[new_source]
|
|
338
|
+
for symbol in range(k):
|
|
339
|
+
target = table[old_source][symbol]
|
|
340
|
+
if target is None or target in old_to_new:
|
|
341
|
+
continue
|
|
342
|
+
old_to_new[target] = next_label
|
|
343
|
+
new_to_old.append(target)
|
|
344
|
+
next_label += 1
|
|
345
|
+
progressed = True
|
|
346
|
+
if not progressed:
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
if len(new_to_old) != n:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
rebuilt = [MISSING_TRANSITION] * (k * n)
|
|
353
|
+
for new_source, old_source in enumerate(new_to_old):
|
|
354
|
+
for symbol in range(k):
|
|
355
|
+
target = table[old_source][symbol]
|
|
356
|
+
if target is None:
|
|
357
|
+
continue
|
|
358
|
+
rebuilt[new_source * k + symbol] = old_to_new[target]
|
|
359
|
+
try:
|
|
360
|
+
validate_idfa_string(rebuilt, n=n, k=k)
|
|
361
|
+
except IDFAEnumerationError:
|
|
362
|
+
return None
|
|
363
|
+
return tuple(rebuilt)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Regular-language algebra for automata constructions."""
|
|
2
|
+
|
|
3
|
+
from sofic.automata.languages.atoms import atoms, is_prime_atom, prime_atoms
|
|
4
|
+
from sofic.automata.languages.base import AutomatonLanguage, ExplicitLanguage, RegularLanguage
|
|
5
|
+
from sofic.automata.languages.operations import (
|
|
6
|
+
complement,
|
|
7
|
+
concat,
|
|
8
|
+
difference,
|
|
9
|
+
intersection,
|
|
10
|
+
kleene_star,
|
|
11
|
+
product,
|
|
12
|
+
reverse,
|
|
13
|
+
union,
|
|
14
|
+
)
|
|
15
|
+
from sofic.automata.languages.quotients import left_quotient, left_quotients, residuals, right_quotient
|
|
16
|
+
from sofic.automata.languages.residuals import is_composed_residual, prime_residuals
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AutomatonLanguage",
|
|
20
|
+
"ExplicitLanguage",
|
|
21
|
+
"RegularLanguage",
|
|
22
|
+
"atoms",
|
|
23
|
+
"complement",
|
|
24
|
+
"concat",
|
|
25
|
+
"difference",
|
|
26
|
+
"intersection",
|
|
27
|
+
"is_composed_residual",
|
|
28
|
+
"is_prime_atom",
|
|
29
|
+
"kleene_star",
|
|
30
|
+
"left_quotient",
|
|
31
|
+
"left_quotients",
|
|
32
|
+
"prime_atoms",
|
|
33
|
+
"prime_residuals",
|
|
34
|
+
"product",
|
|
35
|
+
"residuals",
|
|
36
|
+
"reverse",
|
|
37
|
+
"right_quotient",
|
|
38
|
+
"union",
|
|
39
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shared helpers for quotient and atom computations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.automata.languages.automaton_ops import minimal_dfa_from_language
|
|
9
|
+
from sofic.automata.languages.base import AutomatonLanguage, ExplicitLanguage, RegularLanguage, as_language
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _words_up_to(length: int, alphabet: frozenset[Any]) -> list[tuple[Any, ...]]:
|
|
13
|
+
if length < 0:
|
|
14
|
+
return []
|
|
15
|
+
if length == 0:
|
|
16
|
+
return [()]
|
|
17
|
+
shorter = _words_up_to(length - 1, alphabet)
|
|
18
|
+
return [word + (symbol,) for word in shorter for symbol in alphabet]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _languages_equal(left: RegularLanguage, right: RegularLanguage, alphabet: frozenset[Any], max_len: int = 6) -> bool:
|
|
22
|
+
for length in range(max_len + 1):
|
|
23
|
+
for word in _words_up_to(length, alphabet):
|
|
24
|
+
if (word in left) != (word in right):
|
|
25
|
+
return False
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _residual_from_state(aut: AutomatonLanguage, state) -> AutomatonLanguage:
|
|
30
|
+
sub = aut.automaton.copy()
|
|
31
|
+
sub.initial_states = frozenset({state})
|
|
32
|
+
return AutomatonLanguage(minimal_dfa_from_language(sub))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _alphabet_of(language: RegularLanguage) -> frozenset[Any]:
|
|
36
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
37
|
+
if isinstance(lang, ExplicitLanguage):
|
|
38
|
+
return lang.alphabet
|
|
39
|
+
if isinstance(lang, AutomatonLanguage):
|
|
40
|
+
return lang.automaton.input_alphabet
|
|
41
|
+
return frozenset()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _suffixes_if_prefix(word: tuple[Any, ...], prefix: Sequence[Any]) -> set[tuple[Any, ...]]:
|
|
45
|
+
p = tuple(prefix)
|
|
46
|
+
if word[: len(p)] == p:
|
|
47
|
+
return {word[len(p) :]}
|
|
48
|
+
return set()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _prefixes_if_suffix(word: tuple[Any, ...], suffix: Sequence[Any]) -> set[tuple[Any, ...]]:
|
|
52
|
+
s = tuple(suffix)
|
|
53
|
+
if len(word) >= len(s) and word[-len(s) :] == s:
|
|
54
|
+
return {word[: len(word) - len(s)]}
|
|
55
|
+
return set()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_union_of_others(target: ExplicitLanguage, others: list[ExplicitLanguage]) -> bool:
|
|
59
|
+
union_pos: set[tuple] = set()
|
|
60
|
+
union_neg: set[tuple] = set()
|
|
61
|
+
for lang in others:
|
|
62
|
+
union_pos |= lang._positive
|
|
63
|
+
union_neg |= lang._negative
|
|
64
|
+
return target._positive == union_pos and target._negative == union_neg
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Atoms and prime atoms of regular languages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sofic.automata.languages._quotient_utils import _alphabet_of, _languages_equal, _residual_from_state
|
|
6
|
+
from sofic.automata.languages.automaton_ops import minimal_dfa_from_language
|
|
7
|
+
from sofic.automata.languages.base import AutomatonLanguage, RegularLanguage, as_language
|
|
8
|
+
from sofic.automata.languages.quotients import left_quotients
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def atoms(language: RegularLanguage) -> frozenset[RegularLanguage]:
|
|
12
|
+
lang = as_language(language) # type: ignore[arg-type]
|
|
13
|
+
result: set[RegularLanguage] = set(left_quotients(language))
|
|
14
|
+
if isinstance(lang, AutomatonLanguage) and not result:
|
|
15
|
+
dfa = minimal_dfa_from_language(lang.automaton)
|
|
16
|
+
for state in dfa.states():
|
|
17
|
+
result.add(_residual_from_state(AutomatonLanguage(dfa), state))
|
|
18
|
+
return frozenset(result)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def prime_atoms(language: RegularLanguage) -> frozenset[RegularLanguage]:
|
|
22
|
+
all_atoms = atoms(language)
|
|
23
|
+
return frozenset(atom for atom in all_atoms if is_prime_atom(atom, all_atoms))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_prime_atom(atom: RegularLanguage, all_atoms: frozenset[RegularLanguage]) -> bool:
|
|
27
|
+
others = [candidate for candidate in all_atoms if candidate is not atom]
|
|
28
|
+
if not others:
|
|
29
|
+
return True
|
|
30
|
+
alphabet = _alphabet_of(atom)
|
|
31
|
+
return all(not _languages_equal(atom, other, alphabet) for other in others)
|