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,557 @@
1
+ """Epsilon machines (unifilar causal presentations)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable, Sequence
6
+ from typing import TYPE_CHECKING, Any, Literal, Self
7
+
8
+ from sofic.generators.mealy import MealyHMM
9
+ from sofic.generators.moore import MooreHMM
10
+
11
+ if TYPE_CHECKING:
12
+ from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
13
+ from sofic.generators.block_entropy import BlockEntropyDiagram
14
+ from sofic.generators.minimal_generative_model import (
15
+ FunctionalGenerativeModel,
16
+ GacsKornerGenerativeModel,
17
+ MinimalGenerativeModel,
18
+ WynerGenerativeModel,
19
+ )
20
+
21
+
22
+ class EpsilonMachine(MealyHMM):
23
+ """Unifilar Mealy HMM: minimal causal presentation of a stationary process.
24
+
25
+ Validates row-unifilarity on emissions and exposes computational mechanics
26
+ quantities (entropy rate, statistical complexity, synchronization orders).
27
+
28
+ Examples
29
+ --------
30
+ >>> from sofic.examples import golden_mean
31
+ >>> eps = golden_mean(0.5)
32
+ >>> eps.markov_order()
33
+ 1
34
+ """
35
+
36
+ _bidirectional_machine: BidirectionalEpsilonMachine | None = None
37
+ _bidirectional_machine_fingerprint: tuple[Any, ...] | None = None
38
+
39
+ def validate(self) -> None:
40
+ super().validate()
41
+ self._check_unifilar()
42
+
43
+ @classmethod
44
+ def from_hmm(cls, hmm: MealyHMM | MooreHMM, **kwargs: Any) -> EpsilonMachine:
45
+ from sofic.generators.epsilon_construction import build_epsilon_machine
46
+
47
+ return build_epsilon_machine(hmm)
48
+
49
+ @classmethod
50
+ def from_sequence(
51
+ cls,
52
+ sequence: Sequence[Any],
53
+ *,
54
+ method: Literal["cssr", "subtree", "spectral"] = "cssr",
55
+ **kwargs: Any,
56
+ ) -> EpsilonMachine:
57
+ """Reconstruct an ε-machine from an observed symbol sequence.
58
+
59
+ Parameters
60
+ ----------
61
+ sequence
62
+ Observed process realization.
63
+ method
64
+ ``"cssr"`` for Causal-State Splitting Reconstruction,
65
+ ``"subtree"`` for depth-``L`` subtree merging (pass ``L=...``), or
66
+ ``"spectral"`` for Hankel-SVD learning followed by mixed-state
67
+ extraction.
68
+ **kwargs
69
+ Forwarded to :func:`~sofic.generators.epsilon_inference.cssr`,
70
+ :func:`~sofic.generators.epsilon_inference.subtree_merge`, or
71
+ :func:`~sofic.generators.epsilon_inference.spectral`.
72
+ """
73
+ if method == "cssr":
74
+ from sofic.generators.epsilon_inference import cssr
75
+
76
+ return cssr(sequence, **kwargs)
77
+ if method == "subtree":
78
+ from sofic.generators.epsilon_inference import subtree_merge
79
+
80
+ return subtree_merge(sequence, **kwargs)
81
+ if method == "spectral":
82
+ from sofic.generators.epsilon_inference import spectral
83
+
84
+ return spectral(sequence, **kwargs)
85
+ raise ValueError(f"unknown inference method {method!r}")
86
+
87
+ def copy(self) -> Self:
88
+ cloned = super().copy()
89
+ cloned._bidirectional_machine = None
90
+ cloned._bidirectional_machine_fingerprint = None
91
+ return cloned
92
+
93
+ def invalidate_bidirectional_cache(self) -> None:
94
+ """Clear the cached bidirectional presentation."""
95
+ self._bidirectional_machine = None
96
+ self._bidirectional_machine_fingerprint = None
97
+
98
+ def to_bidirectional(self) -> BidirectionalEpsilonMachine:
99
+ """Return the bidirectional presentation, building and caching on first use."""
100
+ fingerprint = self._bidirectional_cache_fingerprint()
101
+ if self._bidirectional_machine is None or self._bidirectional_machine_fingerprint != fingerprint:
102
+ from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
103
+
104
+ self._bidirectional_machine = BidirectionalEpsilonMachine.from_forward(self)
105
+ self._bidirectional_machine_fingerprint = fingerprint
106
+ return self._bidirectional_machine
107
+
108
+ def _bidirectional_cache_fingerprint(self) -> tuple[Any, ...]:
109
+ states = tuple(
110
+ (
111
+ repr(state),
112
+ tuple(sorted((repr(key), repr(value)) for key, value in self.graph.state_attrs(state).items())),
113
+ )
114
+ for state in sorted(self.states(), key=repr)
115
+ )
116
+ transitions = tuple(
117
+ sorted(
118
+ (
119
+ repr(transition.source),
120
+ repr(transition.target),
121
+ tuple(sorted((repr(key), repr(value)) for key, value in transition.data.items())),
122
+ )
123
+ for transition in self.transitions()
124
+ )
125
+ )
126
+ initial = tuple(sorted((repr(state), float(mass)) for state, mass in self.initial_distribution.items()))
127
+ alphabet = tuple(sorted(repr(symbol) for symbol in self.observation_alphabet))
128
+ return (states, transitions, initial, alphabet)
129
+
130
+ def statistical_complexity(self) -> float:
131
+ """C_mu = H[causal state] under the stationary distribution."""
132
+ return self.state_entropy()
133
+
134
+ def bidirectional_statistical_complexity(self) -> float:
135
+ """C± = H[S⁺, S⁻] under the bidirectional stationary distribution."""
136
+ return self.to_bidirectional().statistical_complexity()
137
+
138
+ def excess_entropy(self) -> float:
139
+ """Excess entropy ``E`` from the bidirectional machine when available."""
140
+ from sofic.generators.block_entropy import _excess_entropy
141
+
142
+ return _excess_entropy(self)
143
+
144
+ def predicted_information(self) -> float:
145
+ """ρ_μ = I[X₀ : S⁺₀] — predicted information rate (James et al., 2013)."""
146
+ return self.to_bidirectional().predicted_information()
147
+
148
+ def bound_information(self) -> float:
149
+ """b_μ = I[X₀ : S⁻₁ | S⁺₀] — bound information rate (James et al., 2013)."""
150
+ return self.to_bidirectional().bound_information()
151
+
152
+ def ephemeral_information(self) -> float:
153
+ """r_μ = H[X₀ | S⁺₀, S⁻₁] — ephemeral information rate (James et al., 2013)."""
154
+ return self.to_bidirectional().ephemeral_information()
155
+
156
+ def structural_ephemeral_information(self) -> float:
157
+ """r_μ^struct = H[S⁺₁ | S⁺₀, S⁻₁] — structural (branching) part of r_μ."""
158
+ return self.to_bidirectional().structural_ephemeral_information()
159
+
160
+ def parallel_edge_information(self) -> float:
161
+ """r_μ^par = H[X₀ | S⁺₀, S⁺₁, S⁻₁] — parallel-edge (gauge) part of r_μ."""
162
+ return self.to_bidirectional().parallel_edge_information()
163
+
164
+ def reverse_structural_ephemeral_information(self) -> float:
165
+ """r̄_μ^struct = H[S⁻₀ | S⁺₀, S⁻₁] — reverse structural (branching) part of r_μ."""
166
+ return self.to_bidirectional().reverse_structural_ephemeral_information()
167
+
168
+ def forward_only_structural_ephemeral(self) -> float:
169
+ """r_μ^fwd = H[S⁺₁ | S⁺₀, S⁻₁, S⁻₀] — forward-only ephemeral branch."""
170
+ return self.to_bidirectional().forward_only_structural_ephemeral()
171
+
172
+ def reverse_only_structural_ephemeral(self) -> float:
173
+ """r_μ^rev = H[S⁻₀ | S⁺₀, S⁻₁, S⁺₁] — reverse-only ephemeral branch."""
174
+ return self.to_bidirectional().reverse_only_structural_ephemeral()
175
+
176
+ def joint_structural_ephemeral(self) -> float:
177
+ """r_μ^joint = I[S⁺₁ : S⁻₀ | S⁺₀, S⁻₁] — joint ephemeral branch."""
178
+ return self.to_bidirectional().joint_structural_ephemeral()
179
+
180
+ def pure_gauge_information(self) -> float:
181
+ """r_μ^gauge = H[X₀ | S⁺₀, S⁻₁, S⁺₁, S⁻₀] — pure-gauge ephemeral branch."""
182
+ return self.to_bidirectional().pure_gauge_information()
183
+
184
+ def bound_structural_information(self) -> float:
185
+ """b_μ^struct = I[S⁺₁ : S⁻₁ | S⁺₀] — structural (branching) part of b_μ."""
186
+ return self.to_bidirectional().bound_structural_information()
187
+
188
+ def bound_parallel_edge_information(self) -> float:
189
+ """b_μ^par = I[X₀ : S⁻₁ | S⁺₀, S⁺₁] — parallel-edge (gauge) part of b_μ."""
190
+ return self.to_bidirectional().bound_parallel_edge_information()
191
+
192
+ def reverse_bound_information(self) -> float:
193
+ """b̄_μ = I[X₀ : S⁺₀ | S⁻₁] — reverse (retrodictive) bound information (= b_μ)."""
194
+ return self.to_bidirectional().reverse_bound_information()
195
+
196
+ def reverse_bound_structural_information(self) -> float:
197
+ """b̄_μ^struct = I[S⁻₀ : S⁺₀ | S⁻₁] — structural part of the reverse bound."""
198
+ return self.to_bidirectional().reverse_bound_structural_information()
199
+
200
+ def reverse_bound_gauge_information(self) -> float:
201
+ """b̄_μ^gauge = I[X₀ : S⁺₀ | S⁻₁, S⁻₀] — reverse bound-gauge (Theorem A′, ≈ 0)."""
202
+ return self.to_bidirectional().reverse_bound_gauge_information()
203
+
204
+ def information_anatomy(self) -> dict[str, float]:
205
+ """Return ρ_μ, b_μ, r_μ, h_μ, E, and bidirectional χ for this ε-machine."""
206
+ return self.to_bidirectional().information_anatomy()
207
+
208
+ def five_variable_anatomy(self) -> dict[str, float]:
209
+ """Full five-variable anatomy: four-atom ephemeral partition + reverse bound mirror."""
210
+ return self.to_bidirectional().five_variable_anatomy()
211
+
212
+ def internal_markov_entropy_rate(self) -> float:
213
+ """h_μ^imc = H[S⁺₁ | S⁺₀] — entropy rate of the internal (causal-state) chain."""
214
+ return self.to_bidirectional().internal_markov_entropy_rate()
215
+
216
+ def reverse_internal_markov_entropy_rate(self) -> float:
217
+ """h̄_μ^imc = H[S⁻₀ | S⁻₁] — entropy rate of the reverse causal-state chain."""
218
+ return self.to_bidirectional().reverse_internal_markov_entropy_rate()
219
+
220
+ def information_diagram(
221
+ self,
222
+ *,
223
+ show_zero: bool = False,
224
+ atoms: str | None = None,
225
+ tol: float = 1e-9,
226
+ ) -> Any:
227
+ """Five-variable information-anatomy I-diagram (31 atoms) over the step joint."""
228
+ return self.to_bidirectional().information_diagram(show_zero=show_zero, atoms=atoms, tol=tol)
229
+
230
+ def plot_information_diagram(self, **kwargs: Any) -> Any:
231
+ """Draw the five-variable information anatomy as a colour-coded UpSet plot."""
232
+ return self.to_bidirectional().plot_information_diagram(**kwargs)
233
+
234
+ def caekl_causal_information(self) -> float:
235
+ """J[S⁺₀ : X₀ : S⁻₁] — CAEKL mutual info among past, present, and future causal states."""
236
+ return self.to_bidirectional().caekl_causal_information()
237
+
238
+ def block_entropy_diagram(self, max_length: int) -> BlockEntropyDiagram:
239
+ """Compute finite-block entropy convergence curves up to ``max_length``."""
240
+ from sofic.generators.block_entropy import block_entropy_diagram
241
+
242
+ return block_entropy_diagram(self, max_length)
243
+
244
+ def block_entropy_estimates(
245
+ self,
246
+ max_length: int,
247
+ *,
248
+ entropy_rate: float | None = None,
249
+ use_exact: bool = True,
250
+ ) -> Any:
251
+ """Approximate information quantities from finite block entropies.
252
+
253
+ With ``use_exact`` (the default) the asymptotic ``h_mu`` and excess entropy
254
+ use the exact closed-form/bidirectional values, matching the diagram and
255
+ block-convergence entry points. Pass ``use_exact=False`` for genuinely
256
+ finite-length estimates (see :meth:`approximate_entropy_rate`).
257
+ """
258
+ from sofic.generators.block_entropy import block_entropy_estimates
259
+
260
+ return block_entropy_estimates(self, max_length, entropy_rate=entropy_rate, use_exact=use_exact)
261
+
262
+ def approximate_entropy_rate(self, max_length: int) -> float:
263
+ """Approximate ``h_mu`` as the last finite-block entropy difference."""
264
+ return self.block_entropy_estimates(max_length, use_exact=False).entropy_rate
265
+
266
+ def approximate_excess_entropy(self, max_length: int, *, entropy_rate: float | None = None) -> float:
267
+ """Approximate ``E`` from finite-block entropy lower/upper estimates."""
268
+ return self.block_entropy_estimates(max_length, entropy_rate=entropy_rate, use_exact=False).excess_entropy
269
+
270
+ def approximate_information_anatomy(
271
+ self,
272
+ max_length: int,
273
+ *,
274
+ entropy_rate: float | None = None,
275
+ ) -> dict[str, float]:
276
+ """Approximate anatomy rates without constructing a bidirectional machine."""
277
+ return self.block_convergence_estimates(max_length, entropy_rate=entropy_rate).information_anatomy()
278
+
279
+ def plot_block_entropy_diagram(self, max_length: int, ax: Any | None = None, **kwargs: Any) -> Any:
280
+ """Compute and plot finite-block entropy convergence curves."""
281
+ from sofic.generators.block_entropy import plot_block_entropy_diagram
282
+
283
+ return plot_block_entropy_diagram(self, max_length, ax=ax, **kwargs)
284
+
285
+ def block_convergence_diagram(self, max_length: int, **kwargs: Any) -> Any:
286
+ """James et al. (2011) block convergence curves up to ``max_length``."""
287
+ from sofic.generators.block_convergence import block_convergence_diagram
288
+
289
+ return block_convergence_diagram(self, max_length, **kwargs)
290
+
291
+ def block_convergence_estimates(
292
+ self,
293
+ max_length: int,
294
+ *,
295
+ entropy_rate: float | None = None,
296
+ use_exact: bool = True,
297
+ max_caekl_length: int | None = None,
298
+ ) -> Any:
299
+ """Finite-block anatomy estimates including TC, DTC, coinformation, and CAEKL."""
300
+ from sofic.generators.block_convergence import block_convergence_estimates
301
+
302
+ return block_convergence_estimates(
303
+ self,
304
+ max_length,
305
+ entropy_rate=entropy_rate,
306
+ use_exact=use_exact,
307
+ max_caekl_length=max_caekl_length,
308
+ )
309
+
310
+ def caekl_block_information(self, length: int) -> float:
311
+ """Block CAEKL mutual information ``J(ℓ)`` from exact word probabilities."""
312
+ from sofic.generators.block_convergence import block_caekl
313
+
314
+ return block_caekl(self, length)
315
+
316
+ def caekl_rate(self, max_length: int, **kwargs: Any) -> float:
317
+ """Asymptotic CAEKL rate ``j_μ`` from finite-block convergence.
318
+
319
+ When :attr:`~sofic.generators.block_convergence.BlockConvergenceEstimates.caekl_rate_converged`
320
+ is ``True``, the returned rate equals ``J(ℓ) - J(ℓ-1)`` for all sufficiently
321
+ large ``ℓ`` in the affine tail.
322
+ """
323
+ return float(self.block_convergence_estimates(max_length, **kwargs).caekl_rate)
324
+
325
+ def caekl_intercept(self, max_length: int, **kwargs: Any) -> float:
326
+ """Subextensive intercept ``J_∞`` in ``J(ℓ) ≈ J_∞ + j_μ ℓ``."""
327
+ return float(self.block_convergence_estimates(max_length, **kwargs).caekl_intercept_scalar)
328
+
329
+ def caekl_rate_converged(self, max_length: int, **kwargs: Any) -> bool:
330
+ """Whether ``j_μ`` is certified from a stable affine tail of ``J(ℓ)``."""
331
+ return bool(self.block_convergence_estimates(max_length, **kwargs).caekl_rate_converged)
332
+
333
+ def plot_block_convergence_diagram(self, max_length: int, ax: Any | None = None, **kwargs: Any) -> Any:
334
+ """Compute and plot James et al. (2011) block convergence curves."""
335
+ from sofic.generators.block_convergence import plot_block_convergence_diagram
336
+
337
+ return plot_block_convergence_diagram(self, max_length, ax=ax, **kwargs)
338
+
339
+ def bidirectional_crypticity(self) -> float:
340
+ """χ = C± − E (bidirectional statistical complexity minus excess entropy)."""
341
+ return self.to_bidirectional().crypticity()
342
+
343
+ def minimal_generative_model(self, **kwargs: Any) -> MinimalGenerativeModel:
344
+ """Construct the minimal-state-entropy generative presentation."""
345
+ return self.to_bidirectional().minimal_generative_model(**kwargs)
346
+
347
+ def wyner_generative_model(self, **kwargs: Any) -> WynerGenerativeModel:
348
+ """Construct the Wyner-common-information generative presentation."""
349
+ return self.to_bidirectional().wyner_generative_model(**kwargs)
350
+
351
+ def functional_generative_model(self, **kwargs: Any) -> FunctionalGenerativeModel:
352
+ """Construct the functional-common-information generative presentation."""
353
+ return self.to_bidirectional().functional_generative_model(**kwargs)
354
+
355
+ def gacs_korner_generative_model(self, **kwargs: Any) -> GacsKornerGenerativeModel:
356
+ """Construct the Gács-Körner (deterministic meet) generative presentation."""
357
+ return self.to_bidirectional().gacs_korner_generative_model(**kwargs)
358
+
359
+ def generative_complexity(self, **kwargs: Any) -> float:
360
+ """C_g = H[G] for the minimal generative model."""
361
+ return self.to_bidirectional().generative_complexity(**kwargs)
362
+
363
+ def crypticity(self) -> float:
364
+ """χ = C_μ − E (forward statistical complexity minus excess entropy)."""
365
+ return self.statistical_complexity() - self.excess_entropy()
366
+
367
+ def causal_irreversibility(self) -> float:
368
+ """ΔC_μ = C_μ − C_μ^rev (Crutchfield et al., 2009)."""
369
+ reverse = self.from_time_reversed(self)
370
+ return self.statistical_complexity() - reverse.statistical_complexity()
371
+
372
+ def stored_information_decomposition(self) -> dict[str, float]:
373
+ """Bidirectional stored-information quantities (Ellison et al., 2009)."""
374
+ bidir = self.to_bidirectional()
375
+ forward = self.statistical_complexity()
376
+ reverse = self.from_time_reversed(self).statistical_complexity()
377
+ bidirectional = bidir.statistical_complexity()
378
+ return {
379
+ "forward_complexity": forward,
380
+ "reverse_complexity": reverse,
381
+ "bidirectional_complexity": bidirectional,
382
+ "causal_irreversibility": forward - reverse,
383
+ "excess_entropy": bidir.excess_entropy(),
384
+ "crypticity": bidir.crypticity(),
385
+ }
386
+
387
+ def transient_information(self, max_length: int) -> float:
388
+ """Finite-block transient information TI(L) (Ellison et al., 2009)."""
389
+ estimates = self.block_entropy_estimates(max_length)
390
+ return float(estimates.transient_information_estimate[max_length])
391
+
392
+ def oracular_information(self, max_length: int) -> float:
393
+ """Finite-block oracular information Ω(L) (Ellison et al., 2009)."""
394
+ estimates = self.block_entropy_estimates(max_length)
395
+ return float(estimates.oracular_information_estimate[max_length])
396
+
397
+ def gauge_information(self, max_length: int) -> float:
398
+ """Finite-block gauge information Γ(L) (Ellison et al., 2009)."""
399
+ estimates = self.block_entropy_estimates(max_length)
400
+ return float(estimates.gauge_information_estimate[max_length])
401
+
402
+ def predictability_gain(self, max_length: int) -> float:
403
+ """Finite-block predictability gain ``PG(L) = h_mu(L) - h_mu`` (Bialek et al., 2001)."""
404
+ if max_length < 1:
405
+ raise ValueError("max_length must be at least 1 for predictability gain")
406
+ estimates = self.block_entropy_estimates(max_length)
407
+ return float(estimates.predictability_gain_estimate[max_length])
408
+
409
+ def structural_information(self) -> float:
410
+ """Asymptotic structural information (Feldman & Crutchfield, 1998)."""
411
+ from sofic.generators.alternative_complexity import structural_information
412
+
413
+ return structural_information(self)
414
+
415
+ def thermodynamic_depth(self) -> float:
416
+ """Thermodynamic depth of causal states (Shalizi & Crutchfield, 1999)."""
417
+ from sofic.generators.alternative_complexity import thermodynamic_depth
418
+
419
+ return thermodynamic_depth(self)
420
+
421
+ def spectral_complexity(self) -> float:
422
+ """Spectral entropy of mixed-state transition eigenvalues (Riechers & Crutchfield, 2017)."""
423
+ from sofic.generators.alternative_complexity import spectral_complexity
424
+
425
+ return spectral_complexity(self)
426
+
427
+ def markov_order(self) -> int | float:
428
+ """Markov order ``R``: longest prefix-free synchronizing word length.
429
+
430
+ Topological (probability-independent); see James et al., arXiv:1010.5545.
431
+ Returns ``math.inf`` when no finite ``R`` exists.
432
+ """
433
+ from sofic.generators.synchronization import graph_from_epsilon_machine, markov_order_from_graph
434
+
435
+ return markov_order_from_graph(graph_from_epsilon_machine(self))
436
+
437
+ def is_markov(self) -> bool:
438
+ """Return whether the process has finite Markov order."""
439
+ import math
440
+
441
+ order = self.markov_order()
442
+ return not isinstance(order, float) or math.isfinite(order)
443
+
444
+ def cryptic_order(self) -> int | float:
445
+ """Cryptic order ``k_chi``: retrodiction depth after synchronization.
446
+
447
+ Distinct from :meth:`crypticity` (``χ = C_μ − E``) and from
448
+ :meth:`~sofic.generators.bidirectional_epsilon_machine.BidirectionalEpsilonMachine.crypticity`
449
+ (``χ = C± − E``). See James et al., arXiv:1010.5545.
450
+ """
451
+ from sofic.generators.synchronization import cryptic_order_from_graph, graph_from_epsilon_machine
452
+
453
+ return cryptic_order_from_graph(graph_from_epsilon_machine(self))
454
+
455
+ def reset_threshold(self) -> int | float:
456
+ """Reset threshold: shortest synchronizing (reset) word length.
457
+
458
+ The complement of :meth:`markov_order` (shortest vs longest start-to-
459
+ singleton path); ``math.inf`` when not exactly synchronizable. See
460
+ James et al., arXiv:1010.5545, and (for the Cerny bound, which applies
461
+ only to complete automata) Volkov (2008).
462
+ """
463
+ from sofic.generators.synchronization import graph_from_epsilon_machine, reset_threshold_from_graph
464
+
465
+ return reset_threshold_from_graph(graph_from_epsilon_machine(self))
466
+
467
+ def synchronizing_word(self) -> list[Any] | None:
468
+ """Return a shortest synchronizing word, or ``None`` if not exactly synchronizable.
469
+
470
+ The list of emitted symbols has length :meth:`reset_threshold`
471
+ (Travers & Crutchfield, arXiv:1008.4182).
472
+ """
473
+ from sofic.generators.synchronization import graph_from_epsilon_machine, shortest_synchronizing_word_from_graph
474
+
475
+ return shortest_synchronizing_word_from_graph(graph_from_epsilon_machine(self))
476
+
477
+ def is_exactly_synchronizable(self) -> bool:
478
+ """Return whether the machine is exactly synchronizable (Travers & Crutchfield, arXiv:1008.4182).
479
+
480
+ True iff a finite synchronizing word exists (equivalently, the reset
481
+ threshold is finite; ``Pr(SYN) = 1``). Strictly weaker than finite
482
+ Markov order: see :meth:`is_definite`.
483
+ """
484
+ from sofic.generators.synchronization import graph_from_epsilon_machine, is_exactly_synchronizable
485
+
486
+ return is_exactly_synchronizable(graph_from_epsilon_machine(self))
487
+
488
+ def is_asymptotically_synchronizable(self) -> bool:
489
+ """Return whether the machine is asymptotically synchronizable (``Pr(WSYN) = 1``).
490
+
491
+ True for every finite-state ε-machine (Travers & Crutchfield,
492
+ arXiv:1008.4182); theorem-backed rather than computed.
493
+ """
494
+ from sofic.generators.synchronization import (
495
+ graph_from_epsilon_machine,
496
+ is_asymptotically_synchronizable_from_graph,
497
+ )
498
+
499
+ return is_asymptotically_synchronizable_from_graph(graph_from_epsilon_machine(self))
500
+
501
+ def is_definite(self) -> bool:
502
+ """Return whether the ε-machine is a definite automaton (finite Markov order).
503
+
504
+ Definiteness (Perles-Rabin-Shamir) means the state is fixed by the last
505
+ ``R`` symbols; equivalent to finite Markov order. Synonym of
506
+ :meth:`is_markov`. Implies :meth:`is_exactly_synchronizable`.
507
+ """
508
+ from sofic.generators.synchronization import graph_from_epsilon_machine, is_definite_from_graph
509
+
510
+ return is_definite_from_graph(graph_from_epsilon_machine(self))
511
+
512
+ @classmethod
513
+ def from_time_reversed(cls, forward: EpsilonMachine) -> EpsilonMachine:
514
+ """Build a reverse ε-machine presentation from ``forward``."""
515
+ from sofic.exceptions import StochasticValidationError, UnifilarityError
516
+ from sofic.generators.reversal import time_reverse_stochastic
517
+
518
+ rev_hmm = time_reverse_stochastic(forward)
519
+ try:
520
+ return cls.from_hmm(rev_hmm)
521
+ except (StochasticValidationError, UnifilarityError):
522
+ return _row_normalized_presentation(rev_hmm)
523
+
524
+
525
+ def _row_normalized_presentation(hmm: MealyHMM) -> EpsilonMachine:
526
+ from sofic.generators.prob import as_prob, simplify_prob
527
+ from sofic.generators.stochastic import normalize_row_weights
528
+ from sofic.graph import ATTR_EMISSION, ATTR_PROB, TransitionGraph
529
+
530
+ graph = TransitionGraph()
531
+ for state in hmm.states():
532
+ graph.add_state(state)
533
+ for state in hmm.states():
534
+ outgoing = list(hmm.graph.out_transitions(state))
535
+ merged: dict[tuple[Hashable, Any], Any] = {}
536
+ for transition in outgoing:
537
+ prob = as_prob(transition.data.get(ATTR_PROB, 0.0))
538
+ emission = transition.data.get(ATTR_EMISSION)
539
+ key = (transition.target, emission)
540
+ if key in merged:
541
+ merged[key] = simplify_prob(as_prob(merged[key]) + as_prob(prob))
542
+ else:
543
+ merged[key] = prob
544
+ merged = normalize_row_weights(merged)
545
+ for (target, emission), prob in merged.items():
546
+ attrs = {ATTR_PROB: as_prob(prob)}
547
+ if emission is not None:
548
+ attrs[ATTR_EMISSION] = emission
549
+ graph.add_transition(state, target, **attrs)
550
+
551
+ eps = EpsilonMachine(
552
+ graph=graph,
553
+ initial_distribution=dict(hmm.initial_distribution),
554
+ observation_alphabet=hmm.observation_alphabet,
555
+ )
556
+ eps.validate_stochastic()
557
+ return eps