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,82 @@
1
+ """Parry measure construction for topological Markov chains."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ from sofic.generators.mealy import MealyHMM
8
+ from sofic.graph import ATTR_EMISSION, ATTR_MULTIPLICITY, ATTR_PROB
9
+ from sofic.shifts.algorithms import adjacency_matrix
10
+ from sofic.shifts.tmc import TopologicalMarkovChain
11
+
12
+
13
+ def _perron_pair(matrix: np.ndarray) -> tuple[float, np.ndarray, np.ndarray]:
14
+ """Return (λ, left, right) Perron eigenvectors with left^T right = 1."""
15
+ eigenvalues, right_vecs = np.linalg.eig(matrix)
16
+ index = int(np.argmax(np.real(eigenvalues)))
17
+ lam = float(np.real(eigenvalues[index]))
18
+ right = np.real(right_vecs[:, index])
19
+ if right[np.argmax(np.abs(right))] < 0.0:
20
+ right = -right
21
+ right = np.maximum(right, 1e-300)
22
+
23
+ left_eigenvalues, left_vecs = np.linalg.eig(matrix.T)
24
+ left_index = int(np.argmax(np.real(left_eigenvalues)))
25
+ left = np.real(left_vecs[:, left_index])
26
+ if left[np.argmax(np.abs(left))] < 0.0:
27
+ left = -left
28
+ left = np.maximum(left, 1e-300)
29
+
30
+ scale = float(left @ right)
31
+ if scale < 0.0:
32
+ left = -left
33
+ scale = -scale
34
+ if scale <= 0.0:
35
+ raise ValueError("failed to normalize Perron eigenvectors")
36
+ left /= scale
37
+ return lam, left, right
38
+
39
+
40
+ def parry_measure(tmc: TopologicalMarkovChain) -> MealyHMM:
41
+ matrix, states = adjacency_matrix(tmc)
42
+ if matrix.size == 0:
43
+ return MealyHMM(initial_distribution={}, observation_alphabet=frozenset())
44
+
45
+ lam, left, right = _perron_pair(matrix)
46
+ n = len(states)
47
+ transition = np.zeros((n, n), dtype=float)
48
+ for i in range(n):
49
+ for j in range(n):
50
+ if matrix[i, j] > 0.0:
51
+ transition[i, j] = matrix[i, j] * right[j] / (lam * right[i])
52
+
53
+ stationary = left * right
54
+ stationary /= stationary.sum()
55
+
56
+ hmm = MealyHMM(
57
+ initial_distribution={states[i]: float(stationary[i]) for i in range(n)},
58
+ observation_alphabet=tmc.symbol_alphabet,
59
+ )
60
+ for state in states:
61
+ hmm.graph.add_state(state)
62
+
63
+ for transition_edge in tmc.transitions():
64
+ i = states.index(transition_edge.source)
65
+ j = states.index(transition_edge.target)
66
+ multiplicity = float(transition_edge.data.get(ATTR_MULTIPLICITY, 1))
67
+ total = matrix[i, j]
68
+ if total <= 0.0:
69
+ continue
70
+ symbol = transition_edge.data.get(ATTR_EMISSION)
71
+ if symbol is None:
72
+ from sofic.graph import ATTR_SYMBOL
73
+
74
+ symbol = transition_edge.data.get(ATTR_SYMBOL)
75
+ weight = transition[i, j] * (multiplicity / total)
76
+ hmm.graph.add_transition(
77
+ transition_edge.source,
78
+ transition_edge.target,
79
+ **{ATTR_PROB: float(weight), ATTR_EMISSION: symbol},
80
+ )
81
+ hmm.validate()
82
+ return hmm
sofic/shifts/sft.py ADDED
@@ -0,0 +1,104 @@
1
+ """Shifts of finite type."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import product
6
+ from typing import Any
7
+
8
+ from sofic.graph import TransitionGraph
9
+ from sofic.shifts.base import SymbolicModel
10
+
11
+
12
+ class ShiftOfFiniteType(SymbolicModel):
13
+ """SFT specified by forbidden words or an explicit presentation."""
14
+
15
+ _forbidden_words: frozenset[tuple[Any, ...]]
16
+ _has_forbidden_word_spec: bool
17
+
18
+ def __init__(
19
+ self,
20
+ forbidden_words: set[tuple[Any, ...]] | frozenset[tuple[Any, ...]] | None = None,
21
+ **kwargs: Any,
22
+ ) -> None:
23
+ super().__init__(**kwargs)
24
+ self._has_forbidden_word_spec = forbidden_words is not None
25
+ self._forbidden_words = frozenset(tuple(word) for word in (forbidden_words or set()))
26
+
27
+ @classmethod
28
+ def from_forbidden_words(
29
+ cls,
30
+ forbidden: set[tuple[Any, ...]],
31
+ symbol_alphabet: frozenset[Any],
32
+ **kwargs: Any,
33
+ ) -> ShiftOfFiniteType:
34
+ from sofic.shifts.sft_construction import from_forbidden_words
35
+
36
+ return from_forbidden_words(forbidden, symbol_alphabet, **kwargs)
37
+
38
+ @classmethod
39
+ def from_presentation(
40
+ cls,
41
+ graph: TransitionGraph,
42
+ symbol_alphabet: frozenset[Any],
43
+ **kwargs: Any,
44
+ ) -> ShiftOfFiniteType:
45
+ return cls(graph=graph, symbol_alphabet=symbol_alphabet, **kwargs)
46
+
47
+ def forbidden_words(
48
+ self,
49
+ *,
50
+ length: int | None = None,
51
+ max_length: int | None = None,
52
+ minimal: bool = False,
53
+ ) -> frozenset[tuple[Any, ...]]:
54
+ """Return forbidden words stored on or inferred from this presentation.
55
+
56
+ With no bounds, this returns the finite defining set supplied to
57
+ :meth:`from_forbidden_words`. For presentation-only SFTs, pass
58
+ ``length`` or ``max_length`` to enumerate forbidden blocks up to that
59
+ bound.
60
+ """
61
+ if length is None and max_length is None:
62
+ if minimal and self._has_forbidden_word_spec:
63
+ return _minimal_forbidden(self._forbidden_words)
64
+ if self._has_forbidden_word_spec:
65
+ return self._forbidden_words
66
+ raise ValueError("pass length or max_length to infer forbidden words from a presentation")
67
+ if length is not None and max_length is not None:
68
+ raise ValueError("pass either length or max_length, not both")
69
+ if length is not None and length < 0:
70
+ raise ValueError("length must be nonnegative")
71
+ if max_length is not None and max_length < 0:
72
+ raise ValueError("max_length must be nonnegative")
73
+ if length is not None:
74
+ lengths = range(length, length + 1)
75
+ else:
76
+ assert max_length is not None
77
+ lengths = range(1, max_length + 1)
78
+ forbidden = set().union(*(self._forbidden_words_by_length(n) for n in lengths))
79
+ return _minimal_forbidden(frozenset(forbidden)) if minimal else frozenset(forbidden)
80
+
81
+ def _forbidden_words_by_length(self, length: int) -> frozenset[tuple[Any, ...]]:
82
+ if length < 0:
83
+ raise ValueError("length must be nonnegative")
84
+ alphabet = sorted(self.symbol_alphabet, key=repr)
85
+ allowed = set(self.factor_language(length))
86
+ candidates = set(product(alphabet, repeat=length))
87
+ return frozenset(candidates - allowed)
88
+
89
+
90
+ def _minimal_forbidden(words: frozenset[tuple[Any, ...]]) -> frozenset[tuple[Any, ...]]:
91
+ minimal: set[tuple[Any, ...]] = set()
92
+ for word in words:
93
+ has_forbidden_subword = False
94
+ for start in range(len(word)):
95
+ for stop in range(start + 1, len(word) + 1):
96
+ subword = word[start:stop]
97
+ if subword != word and subword in words:
98
+ has_forbidden_subword = True
99
+ break
100
+ if has_forbidden_subword:
101
+ break
102
+ if not has_forbidden_subword:
103
+ minimal.add(word)
104
+ return frozenset(minimal)
@@ -0,0 +1,52 @@
1
+ """SFT construction from forbidden words."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from sofic.graph import ATTR_SYMBOL, TransitionGraph
8
+ from sofic.shifts.sft import ShiftOfFiniteType
9
+
10
+
11
+ def from_forbidden_words(
12
+ forbidden: set[tuple[Any, ...]],
13
+ symbol_alphabet: frozenset[Any],
14
+ *,
15
+ max_states: int = 256,
16
+ ) -> ShiftOfFiniteType:
17
+ """Build an SFT presentation via a follower automaton on allowed prefixes."""
18
+ alphabet = tuple(symbol_alphabet)
19
+ forbidden_set = set(forbidden)
20
+ max_len = max((len(word) for word in forbidden_set), default=0)
21
+
22
+ def is_allowed(prefix: tuple[Any, ...]) -> bool:
23
+ return all(not (len(word) <= len(prefix) and prefix[-len(word) :] == word) for word in forbidden_set)
24
+
25
+ graph = TransitionGraph()
26
+ start: tuple[Any, ...] = ()
27
+ graph.add_state(start)
28
+ queue = [start]
29
+ seen = {start}
30
+
31
+ while queue:
32
+ prefix = queue.pop(0)
33
+ if len(seen) >= max_states:
34
+ break
35
+ for symbol in alphabet:
36
+ extended = prefix + (symbol,)
37
+ if not is_allowed(extended):
38
+ continue
39
+ trimmed = extended
40
+ if max_len > 0:
41
+ trimmed = extended[-max_len:]
42
+ if trimmed not in seen:
43
+ seen.add(trimmed)
44
+ graph.add_state(trimmed)
45
+ queue.append(trimmed)
46
+ graph.add_transition(prefix, trimmed, **{ATTR_SYMBOL: symbol})
47
+
48
+ return ShiftOfFiniteType.from_presentation(
49
+ graph,
50
+ symbol_alphabet=symbol_alphabet,
51
+ forbidden_words=forbidden_set,
52
+ )
@@ -0,0 +1,156 @@
1
+ """Sliding block codes (factor maps) between subshifts.
2
+
3
+ A sliding block code is the symbolic-dynamics form of a transducer: a map
4
+ ``Phi`` between shift spaces induced by a local rule on a window of ``memory + 1
5
+ + anticipation`` consecutive symbols (the Curtis-Hedlund-Lyndon theorem
6
+ characterizes exactly the shift-commuting continuous maps this way). See
7
+ Lind & Marcus, *An Introduction to Symbolic Dynamics and Coding* (1995), ch. 1
8
+ and 6.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Iterable, Mapping, Sequence
14
+ from itertools import product
15
+ from typing import Any
16
+
17
+ from sofic.graph import ATTR_OUTPUT, ATTR_SYMBOL, TransitionGraph
18
+ from sofic.shifts.sofic import SoficShift
19
+
20
+
21
+ class SlidingBlockCode:
22
+ """A block map ``Phi`` with given memory and anticipation.
23
+
24
+ ``block_map`` sends each length-``(memory + 1 + anticipation)`` input window to
25
+ a single output symbol. Applying ``Phi`` to a point slides this window,
26
+ emitting one output symbol per position.
27
+
28
+ Examples
29
+ --------
30
+ >>> from sofic.shifts.sliding_block_code import SlidingBlockCode
31
+ >>> nor = SlidingBlockCode(
32
+ ... {("0", "0"): "1", ("0", "1"): "0", ("1", "0"): "0", ("1", "1"): "0"},
33
+ ... memory=1,
34
+ ... anticipation=0,
35
+ ... )
36
+ >>> nor.apply_word(["0", "0", "1", "0"])
37
+ ('1', '0', '0')
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ block_map: Mapping[Sequence[Any], Any],
43
+ *,
44
+ memory: int = 0,
45
+ anticipation: int = 0,
46
+ input_alphabet: Iterable[Any] | None = None,
47
+ output_alphabet: Iterable[Any] | None = None,
48
+ ) -> None:
49
+ if memory < 0 or anticipation < 0:
50
+ raise ValueError("memory and anticipation must be non-negative")
51
+ self.memory = memory
52
+ self.anticipation = anticipation
53
+ self.window = memory + anticipation + 1
54
+ self.block_map: dict[tuple[Any, ...], Any] = {tuple(key): value for key, value in block_map.items()}
55
+ for key in self.block_map:
56
+ if len(key) != self.window:
57
+ raise ValueError(f"block_map key {key!r} has length {len(key)}, expected window {self.window}")
58
+ self.input_alphabet = frozenset(
59
+ input_alphabet if input_alphabet is not None else {symbol for key in self.block_map for symbol in key}
60
+ )
61
+ self.output_alphabet = frozenset(output_alphabet if output_alphabet is not None else self.block_map.values())
62
+
63
+ def __repr__(self) -> str:
64
+ return f"SlidingBlockCode(window={self.window}, memory={self.memory}, anticipation={self.anticipation})"
65
+
66
+ def apply_word(self, word: Sequence[Any]) -> tuple[Any, ...]:
67
+ """Return ``Phi`` applied to a finite word (shrinks by ``window - 1``)."""
68
+ word = tuple(word)
69
+ if len(word) < self.window:
70
+ return ()
71
+ return tuple(self.block_map[word[i : i + self.window]] for i in range(len(word) - self.window + 1))
72
+
73
+ def apply(self, shift: Any) -> SoficShift:
74
+ """Return the image subshift ``Phi(shift)`` as a sofic presentation.
75
+
76
+ Uses the higher-block construction: vertices are allowed
77
+ ``(window - 1)``-blocks of ``shift`` and each allowed ``window``-block
78
+ contributes an edge labeled by its image symbol.
79
+ """
80
+ image = SoficShift(symbol_alphabet=frozenset(self.output_alphabet))
81
+ blocks = list(shift.factor_language(self.window))
82
+ vertices = {block[:-1] for block in blocks} | {block[1:] for block in blocks}
83
+ for vertex in vertices:
84
+ image.graph.add_state(vertex)
85
+ used_outputs: set[Any] = set()
86
+ for block in blocks:
87
+ output = self.block_map.get(block)
88
+ if output is None:
89
+ continue
90
+ image.add_transition(block[:-1], block[1:], output)
91
+ used_outputs.add(output)
92
+ image.symbol_alphabet = frozenset(used_outputs)
93
+ return image.trim_transient()
94
+
95
+ def is_right_resolving(self, shift: Any | None = None) -> bool:
96
+ """Return whether the induced image presentation is right-resolving."""
97
+ source = shift if shift is not None else full_shift(self.input_alphabet)
98
+ return self.apply(source).is_unifilar()
99
+
100
+ def compose(self, other: SlidingBlockCode) -> SlidingBlockCode:
101
+ """Return ``other`` applied after ``self`` (function composition ``other ∘ self``)."""
102
+ window = self.window + other.window - 1
103
+ composed: dict[tuple[Any, ...], Any] = {}
104
+ for word in product(sorted(self.input_alphabet, key=repr), repeat=window):
105
+ middle = self.apply_word(word)
106
+ if len(middle) != other.window:
107
+ continue
108
+ output = other.block_map.get(tuple(middle))
109
+ if output is None:
110
+ continue
111
+ composed[word] = output
112
+ return SlidingBlockCode(
113
+ composed,
114
+ memory=self.memory + other.memory,
115
+ anticipation=self.anticipation + other.anticipation,
116
+ input_alphabet=self.input_alphabet,
117
+ output_alphabet=other.output_alphabet,
118
+ )
119
+
120
+ def to_transducer(self) -> Any:
121
+ """Return a Mealy machine realizing this code (output delayed by anticipation)."""
122
+ from sofic.automata.transducers import MealyMachine
123
+
124
+ graph = TransitionGraph()
125
+ context_length = self.window - 1
126
+ contexts = list(product(sorted(self.input_alphabet, key=repr), repeat=context_length))
127
+ for context in contexts:
128
+ graph.add_state(context)
129
+ used_outputs: set[Any] = set()
130
+ for context in contexts:
131
+ for symbol in sorted(self.input_alphabet, key=repr):
132
+ window = (*context, symbol)
133
+ output = self.block_map.get(window)
134
+ if output is None:
135
+ continue
136
+ target = window[1:]
137
+ graph.add_transition(context, target, **{ATTR_SYMBOL: symbol, ATTR_OUTPUT: output})
138
+ used_outputs.add(output)
139
+ machine = MealyMachine(
140
+ input_alphabet=frozenset(self.input_alphabet),
141
+ output_alphabet=frozenset(used_outputs),
142
+ initial_states=frozenset(contexts),
143
+ graph=graph,
144
+ )
145
+ machine.validate()
146
+ return machine
147
+
148
+
149
+ def full_shift(alphabet: Iterable[Any]) -> SoficShift:
150
+ """Return the full shift over ``alphabet`` as a one-state sofic presentation."""
151
+ symbols = frozenset(alphabet)
152
+ shift = SoficShift(symbol_alphabet=symbols)
153
+ shift.graph.add_state("*")
154
+ for symbol in symbols:
155
+ shift.add_transition("*", "*", symbol)
156
+ return shift
sofic/shifts/sofic.py ADDED
@@ -0,0 +1,111 @@
1
+ """Sofic shift presentations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from sofic.shifts.base import SymbolicModel
8
+
9
+ if TYPE_CHECKING:
10
+ from sofic.generators.mealy import MealyHMM
11
+
12
+
13
+ class SoficShift(SymbolicModel):
14
+ """Sofic subshift given by a labeled directed graph presentation."""
15
+
16
+ def topological_entropy(self) -> float:
17
+ from sofic.shifts.tmc_construction import topological_entropy
18
+
19
+ return topological_entropy(self)
20
+
21
+ def parry_measure(self) -> MealyHMM:
22
+ """Measure of maximal entropy (Parry measure) as a labeled Mealy HMM.
23
+
24
+ The unique measure attaining ``h_top`` (Parry 1964): built from the Perron
25
+ data of a right-resolving presentation of this shift, with each edge's
26
+ emitted symbol preserved. See
27
+ :func:`~sofic.shifts.topological_anatomy.parry_measure_sofic`.
28
+ """
29
+ from sofic.shifts.topological_anatomy import parry_measure_sofic
30
+
31
+ return parry_measure_sofic(self)
32
+
33
+ def topological_anatomy(self) -> dict[str, float]:
34
+ """Topological information anatomy: the MME split ``h_top = b_top + r_top``.
35
+
36
+ Evaluates the metric information anatomy (James et al. 2013) of the
37
+ observed symbol process at the measure of maximal entropy, returning
38
+ ``{h_top, b_top, r_top, excess_entropy}``. Here ``r_top`` is the MME
39
+ erasure entropy rate (Verdu & Weissman 2008). See
40
+ :func:`~sofic.shifts.topological_anatomy.topological_anatomy`.
41
+ """
42
+ from sofic.shifts.topological_anatomy import topological_anatomy
43
+
44
+ return topological_anatomy(self)
45
+
46
+ def markov_order(self) -> int | float:
47
+ """Markov order ``R`` when the presentation is right-resolving (unifilar)."""
48
+ from sofic.generators.synchronization import graph_from_sofic_shift, markov_order_from_graph
49
+
50
+ return markov_order_from_graph(graph_from_sofic_shift(self))
51
+
52
+ def cryptic_order(self) -> int | float:
53
+ """Cryptic order ``k_chi`` when the presentation is right-resolving."""
54
+ from sofic.generators.synchronization import cryptic_order_from_graph, graph_from_sofic_shift
55
+
56
+ return cryptic_order_from_graph(graph_from_sofic_shift(self))
57
+
58
+ def reset_threshold(self) -> int | float:
59
+ """Reset threshold: shortest synchronizing (reset) word length.
60
+
61
+ The complement of :meth:`markov_order`; ``math.inf`` when not exactly
62
+ synchronizable. For the Cerny bound (complete automata only) see Volkov
63
+ (2008).
64
+ """
65
+ from sofic.generators.synchronization import graph_from_sofic_shift, reset_threshold_from_graph
66
+
67
+ return reset_threshold_from_graph(graph_from_sofic_shift(self))
68
+
69
+ def synchronizing_word(self) -> list[Any] | None:
70
+ """Return a shortest synchronizing word, or ``None`` if not exactly synchronizable.
71
+
72
+ The list of symbols has length :meth:`reset_threshold` (Travers &
73
+ Crutchfield, arXiv:1008.4182).
74
+ """
75
+ from sofic.generators.synchronization import graph_from_sofic_shift, shortest_synchronizing_word_from_graph
76
+
77
+ return shortest_synchronizing_word_from_graph(graph_from_sofic_shift(self))
78
+
79
+ def is_exactly_synchronizable(self) -> bool:
80
+ """Return whether this presentation is exactly synchronizable.
81
+
82
+ True iff a finite synchronizing word exists (the reset threshold is
83
+ finite; Travers & Crutchfield, arXiv:1008.4182). Strictly weaker than
84
+ finite Markov order: see :meth:`is_definite`.
85
+ """
86
+ from sofic.generators.synchronization import graph_from_sofic_shift, is_exactly_synchronizable
87
+
88
+ return is_exactly_synchronizable(graph_from_sofic_shift(self))
89
+
90
+ def is_asymptotically_synchronizable(self) -> bool:
91
+ """Return whether this presentation is asymptotically synchronizable.
92
+
93
+ True for every finite-state ε-machine (Travers & Crutchfield,
94
+ arXiv:1008.4182); theorem-backed rather than computed.
95
+ """
96
+ from sofic.generators.synchronization import (
97
+ graph_from_sofic_shift,
98
+ is_asymptotically_synchronizable_from_graph,
99
+ )
100
+
101
+ return is_asymptotically_synchronizable_from_graph(graph_from_sofic_shift(self))
102
+
103
+ def is_definite(self) -> bool:
104
+ """Return whether this presentation is a definite automaton (finite Markov order).
105
+
106
+ Definiteness (Perles-Rabin-Shamir) means the state is fixed by the last
107
+ ``R`` symbols. Implies :meth:`is_exactly_synchronizable`.
108
+ """
109
+ from sofic.generators.synchronization import graph_from_sofic_shift, is_definite_from_graph
110
+
111
+ return is_definite_from_graph(graph_from_sofic_shift(self))
@@ -0,0 +1,110 @@
1
+ """Sofic-Dyck shift presentations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable, Iterator, Sequence
6
+ from typing import Any
7
+
8
+ from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN, Transition
9
+ from sofic.shifts.base import SymbolicModel
10
+
11
+ TransitionRef = tuple[Hashable, Hashable, int]
12
+ MatchedEdge = tuple[TransitionRef, TransitionRef]
13
+
14
+ _KINDS = frozenset({KIND_CALL, KIND_RETURN, KIND_INTERNAL})
15
+
16
+
17
+ def transition_ref(transition: Transition) -> TransitionRef:
18
+ """Return the stable graph reference for ``transition``."""
19
+ return transition.source, transition.target, transition.key
20
+
21
+
22
+ class SoficDyckShift(SymbolicModel):
23
+ """Sofic-Dyck shift presented by a finite Dyck automaton."""
24
+
25
+ call_alphabet: frozenset[Any]
26
+ return_alphabet: frozenset[Any]
27
+ internal_alphabet: frozenset[Any]
28
+ matched_edges: frozenset[MatchedEdge]
29
+
30
+ def __init__(
31
+ self,
32
+ call_alphabet: frozenset[Any] | None = None,
33
+ return_alphabet: frozenset[Any] | None = None,
34
+ internal_alphabet: frozenset[Any] | None = None,
35
+ matched_edges: set[MatchedEdge] | frozenset[MatchedEdge] | None = None,
36
+ symbol_alphabet: frozenset[Any] | None = None,
37
+ **kwargs: Any,
38
+ ) -> None:
39
+ self.call_alphabet = call_alphabet if call_alphabet is not None else frozenset()
40
+ self.return_alphabet = return_alphabet if return_alphabet is not None else frozenset()
41
+ self.internal_alphabet = internal_alphabet if internal_alphabet is not None else frozenset()
42
+ inferred_alphabet = self.call_alphabet | self.return_alphabet | self.internal_alphabet
43
+ super().__init__(
44
+ symbol_alphabet=symbol_alphabet if symbol_alphabet is not None else inferred_alphabet, **kwargs
45
+ )
46
+ self.matched_edges = frozenset(matched_edges or frozenset())
47
+
48
+ def validate(self) -> None:
49
+ super().validate()
50
+ role_alphabet = self.call_alphabet | self.return_alphabet | self.internal_alphabet
51
+ self._require(
52
+ len(self.call_alphabet) + len(self.return_alphabet) + len(self.internal_alphabet) == len(role_alphabet),
53
+ "call, return, and internal alphabets must be disjoint",
54
+ )
55
+ self._require(self.symbol_alphabet == role_alphabet, "symbol_alphabet must equal the visible role alphabets")
56
+
57
+ call_edges: set[TransitionRef] = set()
58
+ return_edges: set[TransitionRef] = set()
59
+ all_edges: set[TransitionRef] = set()
60
+ for transition in self.transitions():
61
+ ref = transition_ref(transition)
62
+ all_edges.add(ref)
63
+ kind = transition.data.get(ATTR_KIND)
64
+ symbol = transition.data.get(ATTR_SYMBOL)
65
+ self._require(kind in _KINDS, f"invalid Dyck edge kind {kind!r}")
66
+ self._require(symbol is not None, "Dyck shift transitions require a symbol")
67
+ if kind == KIND_CALL:
68
+ self._require(symbol in self.call_alphabet, f"{symbol!r} not in call alphabet")
69
+ call_edges.add(ref)
70
+ elif kind == KIND_RETURN:
71
+ self._require(symbol in self.return_alphabet, f"{symbol!r} not in return alphabet")
72
+ return_edges.add(ref)
73
+ else:
74
+ self._require(symbol in self.internal_alphabet, f"{symbol!r} not in internal alphabet")
75
+
76
+ for call_ref, return_ref in self.matched_edges:
77
+ self._require(call_ref in all_edges, f"matched call edge {call_ref!r} is missing")
78
+ self._require(return_ref in all_edges, f"matched return edge {return_ref!r} is missing")
79
+ self._require(call_ref in call_edges, f"matched edge {call_ref!r} is not a call transition")
80
+ self._require(return_ref in return_edges, f"matched edge {return_ref!r} is not a return transition")
81
+
82
+ def add_call_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> TransitionRef:
83
+ """Add a call transition and return its stable edge reference."""
84
+ key = self.graph.add_transition(source, target, **{**attrs, ATTR_KIND: KIND_CALL, ATTR_SYMBOL: symbol})
85
+ return source, target, key
86
+
87
+ def add_return_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> TransitionRef:
88
+ """Add a return transition and return its stable edge reference."""
89
+ key = self.graph.add_transition(source, target, **{**attrs, ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: symbol})
90
+ return source, target, key
91
+
92
+ def add_internal_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> TransitionRef:
93
+ """Add an internal transition and return its stable edge reference."""
94
+ key = self.graph.add_transition(source, target, **{**attrs, ATTR_KIND: KIND_INTERNAL, ATTR_SYMBOL: symbol})
95
+ return source, target, key
96
+
97
+ def add_matched_pair(self, call_ref: TransitionRef, return_ref: TransitionRef) -> None:
98
+ """Mark ``call_ref`` and ``return_ref`` as a legal call-return pair."""
99
+ self.matched_edges = frozenset({*self.matched_edges, (call_ref, return_ref)})
100
+
101
+ def is_admissible_word(self, word: Sequence[Any]) -> bool:
102
+ """Return whether ``word`` is a finite factor of this Dyck presentation."""
103
+ from sofic.shifts.dyck_algorithms import is_admissible_word
104
+
105
+ return is_admissible_word(self, word)
106
+
107
+ def factor_language(self, length: int) -> Iterator[tuple[Any, ...]]:
108
+ from sofic.shifts.dyck_algorithms import admissible_words
109
+
110
+ yield from admissible_words(self, length)
@@ -0,0 +1,64 @@
1
+ """Sofic relations: subshifts over a product alphabet ``X x Y``.
2
+
3
+ The topological support of a transducer is a subshift of the product shift on
4
+ ``X x Y`` -- a "sofic relation" whose input and output projections are the
5
+ transducer's domain and range subshifts. This is the symbolic-dynamics reading
6
+ of a transducer, complementary to the sliding block code (Lind & Marcus, *An
7
+ Introduction to Symbolic Dynamics and Coding* (1995), ch. 6).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from sofic.graph import ATTR_OUTPUT, ATTR_SYMBOL, EPSILON
15
+ from sofic.shifts.sofic import SoficShift
16
+
17
+
18
+ class SoficRelation(SoficShift):
19
+ """A sofic subshift whose symbols are ``(input, output)`` pairs."""
20
+
21
+ @classmethod
22
+ def from_transducer(cls, transducer: Any) -> SoficRelation:
23
+ """Build the topological support of a transducer (probabilities dropped)."""
24
+ relation = cls()
25
+ pairs: set[tuple[Any, Any]] = set()
26
+ for state in transducer.states():
27
+ relation.graph.add_state(state)
28
+ for transition in transducer.transitions():
29
+ input_symbol = transition.data.get(ATTR_SYMBOL, EPSILON)
30
+ output_symbol = transition.data.get(ATTR_OUTPUT, EPSILON)
31
+ pair = (input_symbol, output_symbol)
32
+ relation.add_transition(transition.source, transition.target, pair)
33
+ pairs.add(pair)
34
+ relation.symbol_alphabet = frozenset(pairs)
35
+ return relation
36
+
37
+ def input_alphabet(self) -> frozenset[Any]:
38
+ """Return the set of input symbols appearing in the relation."""
39
+ return frozenset(pair[0] for pair in self.symbol_alphabet)
40
+
41
+ def output_alphabet(self) -> frozenset[Any]:
42
+ """Return the set of output symbols appearing in the relation."""
43
+ return frozenset(pair[1] for pair in self.symbol_alphabet)
44
+
45
+ def input_shift(self) -> SoficShift:
46
+ """Return the projection onto the input coordinate as a sofic shift."""
47
+ return self._project(0)
48
+
49
+ def output_shift(self) -> SoficShift:
50
+ """Return the projection onto the output coordinate as a sofic shift."""
51
+ return self._project(1)
52
+
53
+ def _project(self, index: int) -> SoficShift:
54
+ projected = SoficShift()
55
+ symbols: set[Any] = set()
56
+ for state in self.states():
57
+ projected.graph.add_state(state)
58
+ for transition in self.transitions():
59
+ pair = transition.data.get(ATTR_SYMBOL)
60
+ symbol = pair[index] if isinstance(pair, tuple) else pair
61
+ projected.add_transition(transition.source, transition.target, symbol)
62
+ symbols.add(symbol)
63
+ projected.symbol_alphabet = frozenset(symbols)
64
+ return projected