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/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)