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
@@ -0,0 +1,76 @@
1
+ """Fischer and Krieger cover constructions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from typing import Any
7
+
8
+ from sofic.graph import ATTR_SYMBOL, TransitionGraph
9
+ from sofic.shifts.covers import LeftFischerCover, LeftKriegerCover, RightFischerCover, RightKriegerCover
10
+ from sofic.shifts.sofic import SoficShift
11
+ from sofic.states import sequential_labels
12
+
13
+
14
+ def _follower_language(shift: SoficShift, vertex: Any, max_len: int = 8) -> frozenset[tuple[Any, ...]]:
15
+ from collections import deque
16
+
17
+ seen: set[tuple[Any, ...]] = set()
18
+ queue: deque[tuple[Any, tuple[Any, ...]]] = deque([(vertex, ())])
19
+ while queue:
20
+ state, prefix = queue.popleft()
21
+ if len(prefix) > max_len:
22
+ continue
23
+ if prefix:
24
+ seen.add(prefix)
25
+ for transition in shift.graph.out_transitions(state):
26
+ symbol = transition.data.get(ATTR_SYMBOL)
27
+ if symbol is None:
28
+ continue
29
+ queue.append((transition.target, prefix + (symbol,)))
30
+ return frozenset(seen)
31
+
32
+
33
+ def _build_left_fischer(shift: SoficShift) -> LeftFischerCover:
34
+ followers: dict[Any, frozenset[tuple[Any, ...]]] = {
35
+ vertex: _follower_language(shift, vertex) for vertex in shift.states()
36
+ }
37
+ classes: dict[frozenset[tuple[Any, ...]], list[Any]] = defaultdict(list)
38
+ for vertex, language in followers.items():
39
+ classes[language].append(vertex)
40
+
41
+ graph = TransitionGraph()
42
+ class_for_vertex = {vertex: language for vertex, language in followers.items()}
43
+ state_ids = {language: sequential_labels(len(classes))[index] for index, language in enumerate(classes)}
44
+ for _language, state_id in state_ids.items():
45
+ graph.add_state(state_id)
46
+
47
+ for transition in shift.transitions():
48
+ source_lang = class_for_vertex[transition.source]
49
+ target_lang = class_for_vertex[transition.target]
50
+ symbol = transition.data.get(ATTR_SYMBOL)
51
+ graph.add_transition(state_ids[source_lang], state_ids[target_lang], **{ATTR_SYMBOL: symbol})
52
+
53
+ return LeftFischerCover(
54
+ graph=graph,
55
+ symbol_alphabet=shift.symbol_alphabet,
56
+ )
57
+
58
+
59
+ def left_fischer_from_sofic(shift: SoficShift) -> LeftFischerCover:
60
+ return _build_left_fischer(shift.trim_transient())
61
+
62
+
63
+ def right_fischer_from_sofic(shift: SoficShift) -> RightFischerCover:
64
+ left = left_fischer_from_sofic(shift.reverse())
65
+ return RightFischerCover(
66
+ graph=left.graph.copy(),
67
+ symbol_alphabet=left.symbol_alphabet,
68
+ )
69
+
70
+
71
+ def left_krieger_from_sofic(shift: SoficShift) -> LeftKriegerCover:
72
+ raise NotImplementedError("Left Krieger cover construction is not yet implemented; use left_fischer_from_sofic")
73
+
74
+
75
+ def right_krieger_from_sofic(shift: SoficShift) -> RightKriegerCover:
76
+ raise NotImplementedError("Right Krieger cover construction is not yet implemented; use right_fischer_from_sofic")
sofic/shifts/covers.py ADDED
@@ -0,0 +1,47 @@
1
+ """Fischer and Krieger covers of sofic shifts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from sofic.shifts.sofic import SoficShift
8
+
9
+
10
+ class LeftFischerCover(SoficShift):
11
+ """Left Fischer cover presentation."""
12
+
13
+ @classmethod
14
+ def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> LeftFischerCover:
15
+ from sofic.shifts.cover_construction import left_fischer_from_sofic
16
+
17
+ return left_fischer_from_sofic(shift)
18
+
19
+
20
+ class RightFischerCover(SoficShift):
21
+ """Right Fischer cover presentation."""
22
+
23
+ @classmethod
24
+ def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> RightFischerCover:
25
+ from sofic.shifts.cover_construction import right_fischer_from_sofic
26
+
27
+ return right_fischer_from_sofic(shift)
28
+
29
+
30
+ class LeftKriegerCover(SoficShift):
31
+ """Left Krieger cover presentation."""
32
+
33
+ @classmethod
34
+ def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> LeftKriegerCover:
35
+ from sofic.shifts.cover_construction import left_krieger_from_sofic
36
+
37
+ return left_krieger_from_sofic(shift)
38
+
39
+
40
+ class RightKriegerCover(SoficShift):
41
+ """Right Krieger cover presentation."""
42
+
43
+ @classmethod
44
+ def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> RightKriegerCover:
45
+ from sofic.shifts.cover_construction import right_krieger_from_sofic
46
+
47
+ return right_krieger_from_sofic(shift)
@@ -0,0 +1,100 @@
1
+ """Algorithms for Dyck shift presentations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from collections.abc import Hashable, Iterator, Sequence
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN, Transition
10
+ from sofic.shifts.sofic_dyck import TransitionRef, transition_ref
11
+
12
+ if TYPE_CHECKING:
13
+ from sofic.shifts.sofic_dyck import SoficDyckShift
14
+
15
+ Configuration = tuple[Hashable, tuple[TransitionRef, ...]]
16
+
17
+
18
+ def is_admissible_word(shift: SoficDyckShift, word: Sequence[Any]) -> bool:
19
+ """Return whether ``word`` labels an admissible finite Dyck path."""
20
+ word = tuple(word)
21
+ if not word:
22
+ return True
23
+ if any(symbol not in shift.symbol_alphabet for symbol in word):
24
+ return False
25
+
26
+ current: set[Configuration] = {(state, ()) for state in shift.states()}
27
+ for symbol in word:
28
+ next_configs: set[Configuration] = set()
29
+ for state, stack in current:
30
+ for transition in shift.graph.out_transitions(state):
31
+ if transition.data.get(ATTR_SYMBOL) != symbol:
32
+ continue
33
+ _advance_transition(shift, transition, stack, next_configs)
34
+ if not next_configs:
35
+ return False
36
+ current = next_configs
37
+
38
+ return bool(current)
39
+
40
+
41
+ def admissible_words(shift: SoficDyckShift, length: int) -> Iterator[tuple[Any, ...]]:
42
+ """Yield distinct admissible words of exactly ``length`` symbols."""
43
+ if length <= 0:
44
+ yield ()
45
+ return
46
+
47
+ seen_words: set[tuple[Any, ...]] = set()
48
+ seen_configs: set[tuple[Hashable, tuple[Any, ...], tuple[TransitionRef, ...]]] = set()
49
+ queue: deque[tuple[Hashable, tuple[Any, ...], tuple[TransitionRef, ...]]] = deque()
50
+ for state in shift.states():
51
+ config = (state, (), ())
52
+ queue.append(config)
53
+ seen_configs.add(config)
54
+
55
+ while queue:
56
+ state, prefix, stack = queue.popleft()
57
+ if len(prefix) == length:
58
+ if prefix not in seen_words:
59
+ seen_words.add(prefix)
60
+ yield prefix
61
+ continue
62
+
63
+ for transition in shift.graph.out_transitions(state):
64
+ symbol = transition.data.get(ATTR_SYMBOL)
65
+ if symbol is None:
66
+ continue
67
+ for target, next_stack in _successors(shift, transition, stack):
68
+ config = (target, prefix + (symbol,), next_stack)
69
+ if config in seen_configs:
70
+ continue
71
+ seen_configs.add(config)
72
+ queue.append(config)
73
+
74
+
75
+ def _advance_transition(
76
+ shift: SoficDyckShift,
77
+ transition: Transition,
78
+ stack: tuple[TransitionRef, ...],
79
+ next_configs: set[Configuration],
80
+ ) -> None:
81
+ for target, next_stack in _successors(shift, transition, stack):
82
+ next_configs.add((target, next_stack))
83
+
84
+
85
+ def _successors(
86
+ shift: SoficDyckShift,
87
+ transition: Transition,
88
+ stack: tuple[TransitionRef, ...],
89
+ ) -> Iterator[tuple[Hashable, tuple[TransitionRef, ...]]]:
90
+ kind = transition.data.get(ATTR_KIND)
91
+ ref = transition_ref(transition)
92
+ if kind == KIND_CALL:
93
+ yield transition.target, stack + (ref,)
94
+ elif kind == KIND_RETURN:
95
+ if not stack:
96
+ yield transition.target, stack
97
+ elif (stack[-1], ref) in shift.matched_edges:
98
+ yield transition.target, stack[:-1]
99
+ elif kind == KIND_INTERNAL:
100
+ yield transition.target, stack
@@ -0,0 +1,275 @@
1
+ """Canonical strings and enumeration for small Sofic-Dyck topologies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from collections.abc import Iterator, Sequence
7
+ from dataclasses import dataclass
8
+ from itertools import product
9
+ from typing import Any
10
+
11
+ from sofic.exceptions import SoficValidationError
12
+ from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN
13
+ from sofic.shifts.dyck_algorithms import is_admissible_word
14
+ from sofic.shifts.sofic_dyck import SoficDyckShift, TransitionRef, transition_ref
15
+
16
+ __all__ = [
17
+ "DyckGraphString",
18
+ "count_dyck_graph_strings",
19
+ "dyck_graph_string_to_shift",
20
+ "iter_dyck_graph_strings",
21
+ "iter_sofic_dyck_topologies",
22
+ "shift_to_dyck_graph_string",
23
+ ]
24
+
25
+ _KIND_CALL = 0
26
+ _KIND_RETURN = 1
27
+ _KIND_INTERNAL = 2
28
+
29
+
30
+ class DyckEnumerationError(SoficValidationError):
31
+ """Raised when Dyck-graph enumeration fails."""
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class DyckGraphString:
36
+ """Canonical encoding of a small matched-edge Dyck graph.
37
+
38
+ ``transitions`` lists ``(source, kind, symbol_index, target)`` in
39
+ lexicographic order. ``matched_pairs`` lists ``(call_transition_index,
40
+ return_transition_index)`` into ``transitions``.
41
+ """
42
+
43
+ n: int
44
+ call_symbols: tuple[Any, ...]
45
+ return_symbols: tuple[Any, ...]
46
+ internal_symbols: tuple[Any, ...]
47
+ transitions: tuple[tuple[int, int, int, int], ...]
48
+ matched_pairs: tuple[tuple[int, int], ...]
49
+
50
+
51
+ def shift_to_dyck_graph_string(shift: SoficDyckShift) -> DyckGraphString:
52
+ """Serialize a ``SoficDyckShift`` into canonical edge-index form."""
53
+ states = sorted(shift.states(), key=repr)
54
+ state_index = {state: index for index, state in enumerate(states)}
55
+ call_symbols = tuple(sorted(shift.call_alphabet, key=repr))
56
+ return_symbols = tuple(sorted(shift.return_alphabet, key=repr))
57
+ internal_symbols = tuple(sorted(shift.internal_alphabet, key=repr))
58
+ call_index = {symbol: index for index, symbol in enumerate(call_symbols)}
59
+ return_index = {symbol: index for index, symbol in enumerate(return_symbols)}
60
+ internal_index = {symbol: index for index, symbol in enumerate(internal_symbols)}
61
+
62
+ encoded: list[tuple[int, int, int, int]] = []
63
+ call_positions: dict[TransitionRef, int] = {}
64
+ return_positions: dict[TransitionRef, int] = {}
65
+
66
+ for transition in sorted(shift.transitions(), key=lambda item: (repr(item.source), repr(item.target), item.key)):
67
+ kind = transition.data.get(ATTR_KIND)
68
+ symbol = transition.data.get(ATTR_SYMBOL)
69
+ if kind == KIND_CALL:
70
+ kind_code = _KIND_CALL
71
+ symbol_index = call_index[symbol]
72
+ elif kind == KIND_RETURN:
73
+ kind_code = _KIND_RETURN
74
+ symbol_index = return_index[symbol]
75
+ elif kind == KIND_INTERNAL:
76
+ kind_code = _KIND_INTERNAL
77
+ symbol_index = internal_index[symbol]
78
+ else:
79
+ raise DyckEnumerationError(f"unknown edge kind {kind!r}")
80
+ encoded.append((state_index[transition.source], kind_code, symbol_index, state_index[transition.target]))
81
+ ref = transition_ref(transition)
82
+ position = len(encoded) - 1
83
+ if kind == KIND_CALL:
84
+ call_positions[ref] = position
85
+ elif kind == KIND_RETURN:
86
+ return_positions[ref] = position
87
+
88
+ matched = tuple(
89
+ sorted((call_positions[call_ref], return_positions[return_ref]) for call_ref, return_ref in shift.matched_edges)
90
+ )
91
+
92
+ return DyckGraphString(
93
+ n=len(states),
94
+ call_symbols=call_symbols,
95
+ return_symbols=return_symbols,
96
+ internal_symbols=internal_symbols,
97
+ transitions=tuple(encoded),
98
+ matched_pairs=tuple(sorted(matched)),
99
+ )
100
+
101
+
102
+ def dyck_graph_string_to_shift(spec: DyckGraphString) -> SoficDyckShift:
103
+ """Build a ``SoficDyckShift`` from a ``DyckGraphString``."""
104
+ shift = SoficDyckShift(
105
+ call_alphabet=frozenset(spec.call_symbols),
106
+ return_alphabet=frozenset(spec.return_symbols),
107
+ internal_alphabet=frozenset(spec.internal_symbols),
108
+ )
109
+ states = [f"q{index}" for index in range(spec.n)]
110
+ for state in states:
111
+ shift.graph.add_state(state)
112
+
113
+ edge_refs: list[TransitionRef] = []
114
+ for source, kind_code, symbol_index, target in spec.transitions:
115
+ source_state = states[source]
116
+ target_state = states[target]
117
+ if kind_code == _KIND_CALL:
118
+ symbol = spec.call_symbols[symbol_index]
119
+ edge_refs.append(shift.add_call_transition(source_state, target_state, symbol))
120
+ elif kind_code == _KIND_RETURN:
121
+ symbol = spec.return_symbols[symbol_index]
122
+ edge_refs.append(shift.add_return_transition(source_state, target_state, symbol))
123
+ else:
124
+ symbol = spec.internal_symbols[symbol_index]
125
+ edge_refs.append(shift.add_internal_transition(source_state, target_state, symbol))
126
+
127
+ for call_index, return_index in spec.matched_pairs:
128
+ shift.add_matched_pair(edge_refs[call_index], edge_refs[return_index])
129
+
130
+ shift.validate()
131
+ return shift
132
+
133
+
134
+ def _one_state_transition_choices(
135
+ symbols_per_kind: Sequence[int],
136
+ *,
137
+ include_empty: bool,
138
+ ) -> Iterator[tuple[tuple[int, int, int, int], ...]]:
139
+ """Enumerate transition subsets on a single state looping to itself."""
140
+ slots: list[tuple[int, int, int, int]] = []
141
+ for kind_code, symbol_count in enumerate(symbols_per_kind):
142
+ for symbol_index in range(symbol_count):
143
+ slots.append((0, kind_code, symbol_index, 0))
144
+ if not slots:
145
+ if include_empty:
146
+ yield ()
147
+ return
148
+ for mask in range(1 if not include_empty else 0, 1 << len(slots)):
149
+ selected = tuple(slots[index] for index in range(len(slots)) if mask & (1 << index))
150
+ if selected or include_empty:
151
+ yield selected
152
+
153
+
154
+ def iter_dyck_graph_strings(
155
+ *,
156
+ n: int = 1,
157
+ call_symbols: Sequence[Any] = ("a",),
158
+ return_symbols: Sequence[Any] = ("A",),
159
+ internal_symbols: Sequence[Any] = (),
160
+ include_empty: bool = False,
161
+ ) -> Iterator[DyckGraphString]:
162
+ """Yield canonical Dyck-graph strings for small topologies."""
163
+ if n != 1:
164
+ raise DyckEnumerationError("iter_dyck_graph_strings currently supports n=1 only")
165
+ calls = tuple(call_symbols)
166
+ returns = tuple(return_symbols)
167
+ internals = tuple(internal_symbols)
168
+ if len(calls) != len(returns):
169
+ raise DyckEnumerationError("call and return symbol counts must match")
170
+
171
+ for transitions in _one_state_transition_choices(
172
+ (len(calls), len(returns), len(internals)),
173
+ include_empty=include_empty,
174
+ ):
175
+ call_edge_indices = [index for index, item in enumerate(transitions) if item[1] == _KIND_CALL]
176
+ return_edge_indices = [index for index, item in enumerate(transitions) if item[1] == _KIND_RETURN]
177
+ call_by_symbol: dict[int, list[int]] = defaultdict(list)
178
+ return_by_symbol: dict[int, list[int]] = defaultdict(list)
179
+ for edge_index in call_edge_indices:
180
+ call_by_symbol[transitions[edge_index][2]].append(edge_index)
181
+ for edge_index in return_edge_indices:
182
+ return_by_symbol[transitions[edge_index][2]].append(edge_index)
183
+
184
+ matched_options: list[list[tuple[int, int]]] = []
185
+ for symbol_index in call_by_symbol:
186
+ if symbol_index not in return_by_symbol:
187
+ matched_options = []
188
+ break
189
+ pairs = [
190
+ (call_index, return_index)
191
+ for call_index, return_index in zip(
192
+ call_by_symbol[symbol_index],
193
+ return_by_symbol[symbol_index],
194
+ strict=False,
195
+ )
196
+ ]
197
+ if not pairs:
198
+ matched_options = []
199
+ break
200
+ matched_options.append(pairs)
201
+ if call_by_symbol and not matched_options:
202
+ continue
203
+
204
+ if not matched_options:
205
+ yield DyckGraphString(
206
+ n=1,
207
+ call_symbols=calls,
208
+ return_symbols=returns,
209
+ internal_symbols=internals,
210
+ transitions=transitions,
211
+ matched_pairs=(),
212
+ )
213
+ continue
214
+
215
+ for combo in product(*matched_options):
216
+ pairs: list[tuple[int, int]] = []
217
+ for item in combo:
218
+ if isinstance(item, tuple) and len(item) == 2:
219
+ pairs.append(item)
220
+ matched = tuple(sorted(set(pairs)))
221
+ yield DyckGraphString(
222
+ n=1,
223
+ call_symbols=calls,
224
+ return_symbols=returns,
225
+ internal_symbols=internals,
226
+ transitions=transitions,
227
+ matched_pairs=matched,
228
+ )
229
+
230
+
231
+ def iter_sofic_dyck_topologies(
232
+ *,
233
+ n: int = 1,
234
+ call_symbols: Sequence[Any] = ("a",),
235
+ return_symbols: Sequence[Any] = ("A",),
236
+ internal_symbols: Sequence[Any] = (),
237
+ ) -> Iterator[SoficDyckShift]:
238
+ """Yield valid ``SoficDyckShift`` topologies from canonical Dyck-graph strings."""
239
+ seen: set[tuple[Any, ...]] = set()
240
+ for spec in iter_dyck_graph_strings(
241
+ n=n,
242
+ call_symbols=call_symbols,
243
+ return_symbols=return_symbols,
244
+ internal_symbols=internal_symbols,
245
+ ):
246
+ try:
247
+ shift = dyck_graph_string_to_shift(spec)
248
+ except (SoficValidationError, ValueError):
249
+ continue
250
+ if not is_admissible_word(shift, ()):
251
+ continue
252
+ key = (spec.transitions, spec.matched_pairs)
253
+ if key in seen:
254
+ continue
255
+ seen.add(key)
256
+ yield shift
257
+
258
+
259
+ def count_dyck_graph_strings(
260
+ *,
261
+ n: int = 1,
262
+ call_symbols: Sequence[Any] = ("a",),
263
+ return_symbols: Sequence[Any] = ("A",),
264
+ internal_symbols: Sequence[Any] = (),
265
+ ) -> int:
266
+ """Count canonical Dyck-graph strings for the given signature."""
267
+ return sum(
268
+ 1
269
+ for _spec in iter_dyck_graph_strings(
270
+ n=n,
271
+ call_symbols=call_symbols,
272
+ return_symbols=return_symbols,
273
+ internal_symbols=internal_symbols,
274
+ )
275
+ )
@@ -0,0 +1,172 @@
1
+ """Markov-Dyck shift constructions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable, Sequence
6
+ from typing import Any, Literal
7
+
8
+ import numpy as np
9
+
10
+ from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_RETURN, TransitionGraph
11
+ from sofic.shifts.sofic_dyck import MatchedEdge, SoficDyckShift, TransitionRef, transition_ref
12
+
13
+ GraphKind = Literal["edge", "vertex"]
14
+
15
+
16
+ class MarkovDyckShift(SoficDyckShift):
17
+ """Markov-Dyck shift associated with a finite directed graph or matrix."""
18
+
19
+ @classmethod
20
+ def from_adjacency(
21
+ cls,
22
+ matrix: np.ndarray,
23
+ labels: Sequence[Hashable] | None = None,
24
+ **kwargs: Any,
25
+ ) -> MarkovDyckShift:
26
+ """Build the Markov-Dyck shift from a square adjacency matrix."""
27
+ arr = _as_square_binary_matrix(matrix)
28
+ resolved_labels = _resolve_labels(arr.shape[0], labels)
29
+ return _from_binary_matrix(cls, arr, resolved_labels, **kwargs)
30
+
31
+ @classmethod
32
+ def from_graph(
33
+ cls,
34
+ graph: Any,
35
+ *,
36
+ kind: GraphKind = "edge",
37
+ **kwargs: Any,
38
+ ) -> MarkovDyckShift:
39
+ """Build an edge- or vertex-type Markov-Dyck shift from ``graph``."""
40
+ if kind == "edge":
41
+ matrix, labels = _edge_adjacency(graph)
42
+ elif kind == "vertex":
43
+ matrix, labels = _vertex_adjacency(graph)
44
+ else:
45
+ raise ValueError("kind must be 'edge' or 'vertex'")
46
+ return _from_binary_matrix(cls, matrix, labels, **kwargs)
47
+
48
+
49
+ def _from_binary_matrix(
50
+ cls: type[MarkovDyckShift],
51
+ matrix: np.ndarray,
52
+ labels: tuple[Hashable, ...],
53
+ **kwargs: Any,
54
+ ) -> MarkovDyckShift:
55
+ graph = TransitionGraph()
56
+ call_symbols = tuple((KIND_CALL, label) for label in labels)
57
+ return_symbols = tuple((KIND_RETURN, label) for label in labels)
58
+
59
+ for symbol in (*call_symbols, *return_symbols):
60
+ graph.add_state(symbol)
61
+
62
+ row_intersections = _row_intersections(matrix)
63
+ n = matrix.shape[0]
64
+ for previous_index in range(n):
65
+ for next_index in range(n):
66
+ previous_call = call_symbols[previous_index]
67
+ previous_return = return_symbols[previous_index]
68
+ next_call = call_symbols[next_index]
69
+ next_return = return_symbols[next_index]
70
+
71
+ if matrix[next_index, previous_index]:
72
+ graph.add_transition(previous_call, next_call, **{ATTR_KIND: KIND_CALL, ATTR_SYMBOL: next_call})
73
+ if previous_index == next_index:
74
+ graph.add_transition(previous_call, next_return, **{ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: next_return})
75
+ if row_intersections[next_index, previous_index]:
76
+ graph.add_transition(previous_return, next_call, **{ATTR_KIND: KIND_CALL, ATTR_SYMBOL: next_call})
77
+ if matrix[previous_index, next_index]:
78
+ graph.add_transition(previous_return, next_return, **{ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: next_return})
79
+
80
+ matched_edges = _matched_edges_by_label(graph, labels)
81
+ return cls(
82
+ graph=graph,
83
+ call_alphabet=frozenset(call_symbols),
84
+ return_alphabet=frozenset(return_symbols),
85
+ matched_edges=matched_edges,
86
+ **kwargs,
87
+ )
88
+
89
+
90
+ def _as_square_binary_matrix(matrix: np.ndarray) -> np.ndarray:
91
+ arr = np.asarray(matrix)
92
+ if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
93
+ raise ValueError("adjacency matrix must be square")
94
+ return (arr != 0).astype(bool)
95
+
96
+
97
+ def _resolve_labels(n: int, labels: Sequence[Hashable] | None) -> tuple[Hashable, ...]:
98
+ if labels is None:
99
+ return tuple(range(n))
100
+ resolved = tuple(labels)
101
+ if len(resolved) != n:
102
+ raise ValueError("labels length must match adjacency matrix size")
103
+ if len(frozenset(resolved)) != n:
104
+ raise ValueError("labels must be distinct")
105
+ return resolved
106
+
107
+
108
+ def _row_intersections(matrix: np.ndarray) -> np.ndarray:
109
+ n = matrix.shape[0]
110
+ intersections = np.zeros((n, n), dtype=bool)
111
+ for i in range(n):
112
+ for j in range(n):
113
+ intersections[i, j] = bool(np.any(matrix[i] & matrix[j]))
114
+ return intersections
115
+
116
+
117
+ def _matched_edges_by_label(graph: TransitionGraph, labels: tuple[Hashable, ...]) -> frozenset[MatchedEdge]:
118
+ call_edges: dict[Hashable, set[TransitionRef]] = {label: set() for label in labels}
119
+ return_edges: dict[Hashable, set[TransitionRef]] = {label: set() for label in labels}
120
+ for transition in graph.transitions():
121
+ symbol = transition.data.get(ATTR_SYMBOL)
122
+ if not isinstance(symbol, tuple) or len(symbol) != 2:
123
+ continue
124
+ role, label = symbol
125
+ if role == KIND_CALL:
126
+ call_edges[label].add(transition_ref(transition))
127
+ elif role == KIND_RETURN:
128
+ return_edges[label].add(transition_ref(transition))
129
+
130
+ matched_edges: set[MatchedEdge] = set()
131
+ for label in labels:
132
+ for call_ref in call_edges[label]:
133
+ for return_ref in return_edges[label]:
134
+ matched_edges.add((call_ref, return_ref))
135
+ return frozenset(matched_edges)
136
+
137
+
138
+ def _vertex_adjacency(graph: Any) -> tuple[np.ndarray, tuple[Hashable, ...]]:
139
+ if isinstance(graph, TransitionGraph):
140
+ labels = tuple(graph.states())
141
+ index = {label: i for i, label in enumerate(labels)}
142
+ matrix = np.zeros((len(labels), len(labels)), dtype=bool)
143
+ for transition in graph.transitions():
144
+ matrix[index[transition.target], index[transition.source]] = True
145
+ return matrix, labels
146
+
147
+ labels = tuple(graph.nodes())
148
+ index = {label: i for i, label in enumerate(labels)}
149
+ matrix = np.zeros((len(labels), len(labels)), dtype=bool)
150
+ for source, target in graph.edges():
151
+ matrix[index[target], index[source]] = True
152
+ return matrix, labels
153
+
154
+
155
+ def _edge_adjacency(graph: Any) -> tuple[np.ndarray, tuple[Hashable, ...]]:
156
+ endpoints: tuple[tuple[Hashable, Hashable], ...]
157
+ if isinstance(graph, TransitionGraph):
158
+ transitions = tuple(graph.transitions())
159
+ labels = tuple(transition_ref(transition) for transition in transitions)
160
+ endpoints = tuple((transition.source, transition.target) for transition in transitions)
161
+ elif graph.is_multigraph():
162
+ labels = tuple((source, target, key) for source, target, key in graph.edges(keys=True))
163
+ endpoints = tuple((source, target) for source, target, _key in labels)
164
+ else:
165
+ labels = tuple((source, target) for source, target in graph.edges())
166
+ endpoints = labels
167
+
168
+ matrix = np.zeros((len(labels), len(labels)), dtype=bool)
169
+ for previous, (_source_previous, target_previous) in enumerate(endpoints):
170
+ for next_, (source_next, _target_next) in enumerate(endpoints):
171
+ matrix[next_, previous] = target_previous == source_next
172
+ return matrix, labels