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,689 @@
1
+ """Bidirectional ε-machine — joint forward/reverse causal presentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable
6
+ from typing import Any, Self
7
+
8
+ from sofic.exceptions import SoficValidationError
9
+ from sofic.generators.epsilon_machine import EpsilonMachine
10
+ from sofic.generators.mealy import MealyHMM
11
+
12
+ _STEP_S_PLUS_0 = 0
13
+ _STEP_S_MINUS_0 = 1
14
+ _STEP_X_0 = 2
15
+ _STEP_S_PLUS_1 = 3
16
+ _STEP_S_MINUS_1 = 4
17
+
18
+
19
+ def _require_dit():
20
+ from sofic.generators.measures import require_dit
21
+
22
+ return require_dit("entropy measures")
23
+
24
+
25
+ def _measure_value(dist: Any, value: Any) -> Any:
26
+ """Return a dit measure result as Expr when ``dist`` is symbolic, else float."""
27
+ if hasattr(dist, "is_symbolic") and dist.is_symbolic():
28
+ return value
29
+ return float(value)
30
+
31
+
32
+ class BidirectionalEpsilonMachine(MealyHMM):
33
+ """Non-unifilar generator over joint causal states (S⁺, S⁻).
34
+
35
+ Pairs forward and reverse ε-machines into a single presentation whose states
36
+ are ``(forward, reverse)`` tuples. Supports excess entropy, crypticity, and
37
+ information anatomy when ``dit`` is installed.
38
+
39
+ Examples
40
+ --------
41
+ >>> from sofic.examples import golden_mean_bidirectional
42
+ >>> bidir = golden_mean_bidirectional(0.5)
43
+ >>> bidir.entropy_rate() > 0
44
+ True
45
+ """
46
+
47
+ forward_machine: EpsilonMachine
48
+ reverse_machine: EpsilonMachine
49
+ _joint_pi: dict[tuple[Hashable, Hashable], Any] | None = None
50
+
51
+ def __init__(
52
+ self,
53
+ forward_machine: EpsilonMachine | None = None,
54
+ reverse_machine: EpsilonMachine | None = None,
55
+ **kwargs: Any,
56
+ ) -> None:
57
+ super().__init__(**kwargs)
58
+ if forward_machine is None or reverse_machine is None:
59
+ raise SoficValidationError("forward_machine and reverse_machine are required")
60
+ self.forward_machine = forward_machine
61
+ self.reverse_machine = reverse_machine
62
+
63
+ def validate(self) -> None:
64
+ super().validate_stochastic()
65
+ for state in self.states():
66
+ if not isinstance(state, tuple) or len(state) != 2:
67
+ raise SoficValidationError(f"bidirectional state must be (forward, reverse) pair, got {state!r}")
68
+
69
+ def is_unifilar(self) -> bool:
70
+ """Return whether joint emissions are row-unifilar (usually ``False``)."""
71
+ from sofic.properties import is_unifilar_emissions
72
+
73
+ return is_unifilar_emissions(self)
74
+
75
+ def entropy_rate(self) -> Any:
76
+ """Process entropy rate h_μ (same as the forward ε-machine)."""
77
+ return self.forward_machine.entropy_rate()
78
+
79
+ def copy(self) -> Self:
80
+ cloned = super().copy()
81
+ cloned._joint_pi = None
82
+ return cloned
83
+
84
+ @classmethod
85
+ def from_pair(
86
+ cls,
87
+ forward: EpsilonMachine,
88
+ reverse: EpsilonMachine,
89
+ ) -> BidirectionalEpsilonMachine:
90
+ from sofic.generators.bidirectional_construction import build_bidirectional_epsilon_machine
91
+
92
+ return build_bidirectional_epsilon_machine(forward, reverse)
93
+
94
+ @classmethod
95
+ def from_forward(cls, forward: EpsilonMachine) -> BidirectionalEpsilonMachine:
96
+ from sofic.generators.bidirectional_construction import infer_reverse_epsilon_machine
97
+
98
+ reverse = infer_reverse_epsilon_machine(forward)
99
+ return cls.from_pair(forward, reverse)
100
+
101
+ def joint_distribution(self) -> dict[tuple[Hashable, Hashable], Any]:
102
+ if self._joint_pi is not None:
103
+ return dict(self._joint_pi)
104
+ from sofic.generators.bidirectional_construction import joint_distribution
105
+
106
+ return joint_distribution(self)
107
+
108
+ def forward_epsilon_machine(self) -> EpsilonMachine:
109
+ from sofic.generators.bidirectional_construction import forward_epsilon_machine
110
+
111
+ return forward_epsilon_machine(self)
112
+
113
+ def reverse_epsilon_machine(self) -> EpsilonMachine:
114
+ from sofic.generators.bidirectional_construction import reverse_epsilon_machine
115
+
116
+ return reverse_epsilon_machine(self)
117
+
118
+ def step_distribution(self) -> Any:
119
+ from sofic.generators.bidirectional_construction import bidirectional_step_distribution
120
+
121
+ return bidirectional_step_distribution(self)
122
+
123
+ def predicted_information(self) -> Any:
124
+ """ρ_μ = I[X₀ : S⁺₀] — predicted information rate (James et al., 2013)."""
125
+ dit = _require_dit()
126
+ dist = self.step_distribution()
127
+ return _measure_value(dist, dit.shannon.mutual_information(dist, [_STEP_X_0], [_STEP_S_PLUS_0]))
128
+
129
+ def bound_information(self) -> Any:
130
+ """b_μ = I[X₀ : S⁻₁ | S⁺₀] — bound information rate (James et al., 2013)."""
131
+ dit = _require_dit()
132
+ dist = self.step_distribution()
133
+ return _measure_value(
134
+ dist,
135
+ dit.shannon.conditional_entropy(dist, [_STEP_X_0], [_STEP_S_PLUS_0])
136
+ - dit.shannon.conditional_entropy(
137
+ dist,
138
+ [_STEP_X_0],
139
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1],
140
+ ),
141
+ )
142
+
143
+ def ephemeral_information(self) -> Any:
144
+ """r_μ = H[X₀ | S⁺₀, S⁻₁] — ephemeral information rate (James et al., 2013)."""
145
+ dit = _require_dit()
146
+ dist = self.step_distribution()
147
+ return _measure_value(
148
+ dist,
149
+ dit.shannon.conditional_entropy(
150
+ dist,
151
+ [_STEP_X_0],
152
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1],
153
+ ),
154
+ )
155
+
156
+ def structural_ephemeral_information(self) -> Any:
157
+ """r_μ^struct = H[S⁺₁ | S⁺₀, S⁻₁] — structural (branching) part of r_μ.
158
+
159
+ The next forward causal state S⁺₁ is a deterministic function of S⁺₀ and
160
+ X₀, so this equals I[X₀ : S⁺₁ | S⁺₀, S⁻₁]: the ephemeral randomness that
161
+ selects among transitions to *different* next states (edges with
162
+ structural consequence) and is not resolved by the future S⁻₁. Together
163
+ with :meth:`parallel_edge_information` it partitions
164
+ :meth:`ephemeral_information` (r_μ = r_μ^struct + r_μ^par).
165
+
166
+ This refinement of the information anatomy (James et al., 2013) has no
167
+ separate canonical source; it follows from the determinism of the
168
+ forward transition function.
169
+ """
170
+ dit = _require_dit()
171
+ dist = self.step_distribution()
172
+ return _measure_value(
173
+ dist,
174
+ dit.shannon.conditional_entropy(
175
+ dist,
176
+ [_STEP_S_PLUS_1],
177
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1],
178
+ ),
179
+ )
180
+
181
+ def parallel_edge_information(self) -> Any:
182
+ """r_μ^par = H[X₀ | S⁺₀, S⁺₁, S⁻₁] — parallel-edge (gauge) part of r_μ.
183
+
184
+ Once the source S⁺₀ and destination S⁺₁ forward causal states are both
185
+ fixed, the residual symbol uncertainty is pure output relabeling on edges
186
+ "from the same state to the same state" — no structural consequence. It
187
+ is the gauge component of the ephemeral information, complementary to
188
+ :meth:`structural_ephemeral_information` (r_μ = r_μ^struct + r_μ^par).
189
+
190
+ Refinement of the information anatomy (James et al., 2013); no separate
191
+ canonical source.
192
+ """
193
+ dit = _require_dit()
194
+ dist = self.step_distribution()
195
+ return _measure_value(
196
+ dist,
197
+ dit.shannon.conditional_entropy(
198
+ dist,
199
+ [_STEP_X_0],
200
+ [_STEP_S_PLUS_0, _STEP_S_PLUS_1, _STEP_S_MINUS_1],
201
+ ),
202
+ )
203
+
204
+ def reverse_structural_ephemeral_information(self) -> Any:
205
+ """r̄_μ^struct = H[S⁻₀ | S⁺₀, S⁻₁] — reverse structural (branching) part of r_μ.
206
+
207
+ Time-reversed mirror of :meth:`structural_ephemeral_information`. The
208
+ previous reverse causal state S⁻₀ is a deterministic function of S⁻₁ and
209
+ X₀, so this equals I[X₀ : S⁻₀ | S⁺₀, S⁻₁]: the ephemeral randomness that
210
+ the *past* cannot foresee yet which selects among *different* retrodictive
211
+ (reverse) states — the branch the reverse ε-machine must resolve. It
212
+ partitions as r̄_μ^struct = r_μ^rev + r_μ^joint (see
213
+ :meth:`reverse_only_structural_ephemeral` and
214
+ :meth:`joint_structural_ephemeral`).
215
+
216
+ Refinement of the bidirectional information taxonomy
217
+ (:cite:`jurgens2026taxonomy`; James et al., 2013); follows from the
218
+ determinism of the reverse transition function.
219
+ """
220
+ dit = _require_dit()
221
+ dist = self.step_distribution()
222
+ return _measure_value(
223
+ dist,
224
+ dit.shannon.conditional_entropy(
225
+ dist,
226
+ [_STEP_S_MINUS_0],
227
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1],
228
+ ),
229
+ )
230
+
231
+ def forward_only_structural_ephemeral(self) -> Any:
232
+ """r_μ^fwd = H[S⁺₁ | S⁺₀, S⁻₁, S⁻₀] — forward-only ephemeral branch.
233
+
234
+ One of the four atoms of the five-variable ephemeral partition
235
+ r_μ = r_μ^fwd + r_μ^rev + r_μ^joint + r_μ^gauge. It is the present
236
+ randomness that changes the *next forward* causal state S⁺₁ while leaving
237
+ the *previous reverse* causal state S⁻₀ (and the future S⁻₁) unresolved:
238
+ a branch the future forgets but that the reverse presentation never even
239
+ sees. In the taxonomy of :cite:`jurgens2026taxonomy` this is the
240
+ persistent forward ephemeral rate ``p.r⁺_μ``.
241
+ """
242
+ dit = _require_dit()
243
+ dist = self.step_distribution()
244
+ return _measure_value(
245
+ dist,
246
+ dit.shannon.conditional_entropy(
247
+ dist,
248
+ [_STEP_S_PLUS_1],
249
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1, _STEP_S_MINUS_0],
250
+ ),
251
+ )
252
+
253
+ def reverse_only_structural_ephemeral(self) -> Any:
254
+ """r_μ^rev = H[S⁻₀ | S⁺₀, S⁻₁, S⁺₁] — reverse-only ephemeral branch.
255
+
256
+ Time-reversed mirror of :meth:`forward_only_structural_ephemeral` and one
257
+ of the four atoms of r_μ = r_μ^fwd + r_μ^rev + r_μ^joint + r_μ^gauge. It
258
+ is the present randomness that changes the *previous reverse* causal state
259
+ S⁻₀ while leaving the *next forward* state S⁺₁ (and the future S⁻₁)
260
+ unresolved — the branch only the retrodictor must resolve. In the taxonomy
261
+ of :cite:`jurgens2026taxonomy` this is the persistent reverse ephemeral
262
+ rate ``p.r⁻_μ`` and is a clean arrow-of-time diagnostic: it can be nonzero
263
+ while r_μ^fwd vanishes (e.g. the noisy random phase-slip process).
264
+ """
265
+ dit = _require_dit()
266
+ dist = self.step_distribution()
267
+ return _measure_value(
268
+ dist,
269
+ dit.shannon.conditional_entropy(
270
+ dist,
271
+ [_STEP_S_MINUS_0],
272
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1, _STEP_S_PLUS_1],
273
+ ),
274
+ )
275
+
276
+ def joint_structural_ephemeral(self) -> Any:
277
+ """r_μ^joint = I[S⁺₁ : S⁻₀ | S⁺₀, S⁻₁] — joint ephemeral branch.
278
+
279
+ One of the four atoms of r_μ = r_μ^fwd + r_μ^rev + r_μ^joint + r_μ^gauge:
280
+ the present randomness that *simultaneously* selects the next forward
281
+ state S⁺₁ and the previous reverse state S⁻₀ — a single coin flip both
282
+ presentations must branch on but which the future S⁻₁ still forgets. It is
283
+ shared by the forward and reverse structural ephemeral rates
284
+ (r_μ^struct = r_μ^fwd + r_μ^joint and r̄_μ^struct = r_μ^rev + r_μ^joint),
285
+ and equals the persistent bidirectional ephemeral rate ``p.r±_μ`` of
286
+ :cite:`jurgens2026taxonomy`. Nonzero for the golden mean.
287
+ """
288
+ dit = _require_dit()
289
+ dist = self.step_distribution()
290
+ return _measure_value(
291
+ dist,
292
+ dit.shannon.conditional_entropy(dist, [_STEP_S_PLUS_1], [_STEP_S_PLUS_0, _STEP_S_MINUS_1])
293
+ - dit.shannon.conditional_entropy(
294
+ dist,
295
+ [_STEP_S_PLUS_1],
296
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1, _STEP_S_MINUS_0],
297
+ ),
298
+ )
299
+
300
+ def pure_gauge_information(self) -> Any:
301
+ """r_μ^gauge = H[X₀ | S⁺₀, S⁻₁, S⁺₁, S⁻₀] — pure-gauge ephemeral branch.
302
+
303
+ The fourth atom of r_μ = r_μ^fwd + r_μ^rev + r_μ^joint + r_μ^gauge: once
304
+ the source S⁺₀, both next states S⁺₁ and S⁻₀, and the future S⁻₁ are all
305
+ fixed, the residual symbol uncertainty is pure output relabeling on edges
306
+ with *no* structural consequence in either time direction — parallel edges
307
+ from the same state to the same state. It is the transient ephemeral rate
308
+ ``t.r_μ`` of :cite:`jurgens2026taxonomy` and is the whole of r_μ for an
309
+ i.i.d. fair coin. Sharper than :meth:`parallel_edge_information`, which
310
+ still lumps in the reverse-only branch (r_μ^par = r_μ^rev + r_μ^gauge).
311
+ """
312
+ dit = _require_dit()
313
+ dist = self.step_distribution()
314
+ return _measure_value(
315
+ dist,
316
+ dit.shannon.conditional_entropy(
317
+ dist,
318
+ [_STEP_X_0],
319
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1, _STEP_S_PLUS_1, _STEP_S_MINUS_0],
320
+ ),
321
+ )
322
+
323
+ def bound_structural_information(self) -> Any:
324
+ """b_μ^struct = I[S⁺₁ : S⁻₁ | S⁺₀] — structural (branching) part of b_μ.
325
+
326
+ Companion to :meth:`structural_ephemeral_information`: the transition
327
+ randomness that changes the next forward causal state *and* is shared
328
+ with the future S⁻₁. Together with :meth:`bound_parallel_edge_information`
329
+ it partitions :meth:`bound_information` (b_μ = b_μ^struct + b_μ^par).
330
+
331
+ Refinement of the information anatomy (James et al., 2013); no separate
332
+ canonical source.
333
+ """
334
+ dit = _require_dit()
335
+ dist = self.step_distribution()
336
+ return _measure_value(
337
+ dist,
338
+ dit.shannon.conditional_entropy(dist, [_STEP_S_PLUS_1], [_STEP_S_PLUS_0])
339
+ - dit.shannon.conditional_entropy(
340
+ dist,
341
+ [_STEP_S_PLUS_1],
342
+ [_STEP_S_PLUS_0, _STEP_S_MINUS_1],
343
+ ),
344
+ )
345
+
346
+ def bound_parallel_edge_information(self) -> Any:
347
+ """b_μ^par = I[X₀ : S⁻₁ | S⁺₀, S⁺₁] — parallel-edge (gauge) part of b_μ.
348
+
349
+ Companion to :meth:`parallel_edge_information`: the output-relabeling
350
+ randomness on a fixed transition (source and destination forward states
351
+ held constant) that is nonetheless shared with the future S⁻₁. Together
352
+ with :meth:`bound_structural_information` it partitions
353
+ :meth:`bound_information` (b_μ = b_μ^struct + b_μ^par).
354
+
355
+ Refinement of the information anatomy (James et al., 2013); no separate
356
+ canonical source.
357
+ """
358
+ dit = _require_dit()
359
+ dist = self.step_distribution()
360
+ return _measure_value(
361
+ dist,
362
+ dit.shannon.conditional_entropy(dist, [_STEP_X_0], [_STEP_S_PLUS_0, _STEP_S_PLUS_1])
363
+ - dit.shannon.conditional_entropy(
364
+ dist,
365
+ [_STEP_X_0],
366
+ [_STEP_S_PLUS_0, _STEP_S_PLUS_1, _STEP_S_MINUS_1],
367
+ ),
368
+ )
369
+
370
+ def reverse_bound_information(self) -> Any:
371
+ """b̄_μ = I[X₀ : S⁺₀ | S⁻₁] — reverse (retrodictive) bound information.
372
+
373
+ Time-reversed mirror of :meth:`bound_information`: the present information
374
+ shared with the *past* causal state S⁺₀ but not already carried by the
375
+ future S⁻₁. Bound information is time-reversal invariant, so b̄_μ = b_μ
376
+ (:cite:`James2011`); this method computes it from the reverse triple and
377
+ the equality is asserted in the test-suite as a symmetry check.
378
+ """
379
+ dit = _require_dit()
380
+ dist = self.step_distribution()
381
+ return _measure_value(
382
+ dist,
383
+ dit.shannon.conditional_entropy(dist, [_STEP_X_0], [_STEP_S_MINUS_1])
384
+ - dit.shannon.conditional_entropy(
385
+ dist,
386
+ [_STEP_X_0],
387
+ [_STEP_S_MINUS_1, _STEP_S_PLUS_0],
388
+ ),
389
+ )
390
+
391
+ def reverse_bound_structural_information(self) -> Any:
392
+ """b̄_μ^struct = I[S⁻₀ : S⁺₀ | S⁻₁] — structural part of the reverse bound.
393
+
394
+ Time-reversed mirror of :meth:`bound_structural_information`: the branch
395
+ that changes the previous reverse causal state S⁻₀ *and* is shared with
396
+ the past S⁺₀. By Theorem A′ the reverse bound has no gauge part
397
+ (:meth:`reverse_bound_gauge_information` ≈ 0), so b̄_μ^struct = b̄_μ = b_μ.
398
+
399
+ Refinement of the bidirectional information taxonomy
400
+ (:cite:`jurgens2026taxonomy`; James et al., 2013).
401
+ """
402
+ dit = _require_dit()
403
+ dist = self.step_distribution()
404
+ return _measure_value(
405
+ dist,
406
+ dit.shannon.conditional_entropy(dist, [_STEP_S_MINUS_0], [_STEP_S_MINUS_1])
407
+ - dit.shannon.conditional_entropy(
408
+ dist,
409
+ [_STEP_S_MINUS_0],
410
+ [_STEP_S_MINUS_1, _STEP_S_PLUS_0],
411
+ ),
412
+ )
413
+
414
+ def reverse_bound_gauge_information(self) -> Any:
415
+ """b̄_μ^gauge = I[X₀ : S⁺₀ | S⁻₁, S⁻₀] — reverse bound-gauge (Theorem A′).
416
+
417
+ Time-reversed mirror of :meth:`bound_parallel_edge_information`. Theorem A′
418
+ (the reverse of James et al.'s bound-gauge vanishing, Theorem A) asserts
419
+ this is identically zero: once the previous reverse state S⁻₀ is fixed,
420
+ the present symbol X₀ carries nothing further about the past S⁺₀ that the
421
+ future S⁻₁ did not already supply. Computed here to verify the theorem
422
+ numerically; the test-suite pins it to ≈ 0.
423
+
424
+ Refinement of the bidirectional information taxonomy
425
+ (:cite:`jurgens2026taxonomy`; James et al., 2013).
426
+ """
427
+ dit = _require_dit()
428
+ dist = self.step_distribution()
429
+ return _measure_value(
430
+ dist,
431
+ dit.shannon.conditional_entropy(dist, [_STEP_X_0], [_STEP_S_MINUS_1, _STEP_S_MINUS_0])
432
+ - dit.shannon.conditional_entropy(
433
+ dist,
434
+ [_STEP_X_0],
435
+ [_STEP_S_MINUS_1, _STEP_S_MINUS_0, _STEP_S_PLUS_0],
436
+ ),
437
+ )
438
+
439
+ def internal_markov_entropy_rate(self) -> Any:
440
+ """h_μ^imc = H[S⁺₁ | S⁺₀] — entropy rate of the internal (causal-state) chain.
441
+
442
+ The ε-machine's causal states form a stationary Markov chain; this is its
443
+ entropy rate. By forward unifilarity (S⁺₁ = φ⁺(S⁺₀, X₀)) it equals
444
+ I[X₀ : S⁺₁ | S⁺₀] — the present randomness that *changes the next forward
445
+ state* — and decomposes through the anatomy as
446
+
447
+ h_μ^imc = b_μ + r_μ^fwd + r_μ^joint = h_μ − r_μ^rev − r_μ^gauge,
448
+
449
+ i.e. the process entropy rate with the parallel-edge (pure output
450
+ relabeling) randomness removed.
451
+
452
+ This is **not** time-reversal symmetric in general. The reverse
453
+ causal-state chain rate :meth:`reverse_internal_markov_entropy_rate`
454
+ differs by r_μ^fwd − r_μ^rev — the arrow-of-time asymmetry — and the two
455
+ coincide exactly when r_μ^fwd = r_μ^rev (e.g. the golden mean, even
456
+ process and Nemo, but not the noisy random phase slip or the butterfly).
457
+
458
+ Refinement of the information anatomy (:cite:`James2011`); no separate
459
+ canonical source.
460
+ """
461
+ dit = _require_dit()
462
+ dist = self.step_distribution()
463
+ return _measure_value(
464
+ dist,
465
+ dit.shannon.conditional_entropy(dist, [_STEP_S_PLUS_1], [_STEP_S_PLUS_0]),
466
+ )
467
+
468
+ def reverse_internal_markov_entropy_rate(self) -> Any:
469
+ """h̄_μ^imc = H[S⁻₀ | S⁻₁] — entropy rate of the reverse causal-state chain.
470
+
471
+ Time-reversed mirror of :meth:`internal_markov_entropy_rate`. By reverse
472
+ unifilarity (S⁻₀ = φ⁻(S⁻₁, X₀)) it equals I[X₀ : S⁻₀ | S⁻₁] and
473
+
474
+ h̄_μ^imc = b_μ + r_μ^rev + r_μ^joint = h_μ − r_μ^fwd − r_μ^gauge.
475
+
476
+ Equals :meth:`internal_markov_entropy_rate` iff r_μ^fwd = r_μ^rev; the
477
+ difference h_μ^imc − h̄_μ^imc = r_μ^fwd − r_μ^rev is an arrow-of-time
478
+ diagnostic (:cite:`jurgens2026taxonomy`; James et al., 2013).
479
+ """
480
+ dit = _require_dit()
481
+ dist = self.step_distribution()
482
+ return _measure_value(
483
+ dist,
484
+ dit.shannon.conditional_entropy(dist, [_STEP_S_MINUS_0], [_STEP_S_MINUS_1]),
485
+ )
486
+
487
+ def caekl_causal_information(self) -> Any:
488
+ """J[S⁺₀ : X₀ : S⁻₁] — CAEKL mutual information among past, present, and future.
489
+
490
+ Chan-AlBashabsheh-Ebrahimi-Kaced-Liu multivariate mutual information
491
+ (:cite:`chan2015multivariate`) over the information-anatomy triple
492
+ (:cite:`James2013`): the forward causal state S⁺₀ (past), the present
493
+ symbol X₀, and the reverse causal state S⁻₁ (future). Finite and
494
+ closed-form since the causal states are finite sufficient statistics of
495
+ the semi-infinite past and future.
496
+ """
497
+ _require_dit()
498
+ from dit.multivariate import caekl_mutual_information
499
+
500
+ dist = self.step_distribution()
501
+ return _measure_value(
502
+ dist,
503
+ caekl_mutual_information(dist, rvs=[[_STEP_S_PLUS_0], [_STEP_X_0], [_STEP_S_MINUS_1]]),
504
+ )
505
+
506
+ def excess_entropy(self) -> Any:
507
+ """Exact excess entropy E = I[S⁺; S⁻] from the bidirectional joint distribution."""
508
+ dit = _require_dit()
509
+ joint = self.joint_distribution()
510
+ if not joint:
511
+ return 0.0
512
+
513
+ from sofic.generators.prob import as_prob, has_symbolic, sum_probs
514
+
515
+ pi_plus: dict[Any, Any] = {}
516
+ pi_minus: dict[Any, Any] = {}
517
+ for (alpha, gamma), mass in joint.items():
518
+ pi_plus[alpha] = sum_probs([pi_plus.get(alpha, 0), mass])
519
+ pi_minus[gamma] = sum_probs([pi_minus.get(gamma, 0), mass])
520
+
521
+ plus_outcomes = list(pi_plus.keys())
522
+ minus_outcomes = list(pi_minus.keys())
523
+ joint_outcomes = list(joint.keys())
524
+ plus_pmf = [as_prob(pi_plus[s]) for s in plus_outcomes]
525
+ minus_pmf = [as_prob(pi_minus[s]) for s in minus_outcomes]
526
+ joint_pmf = [as_prob(joint[outcome]) for outcome in joint_outcomes]
527
+ if has_symbolic(joint_pmf):
528
+ from dit.symbolic import symbolic_distribution
529
+
530
+ plus_dist = symbolic_distribution(plus_outcomes, plus_pmf)
531
+ minus_dist = symbolic_distribution(minus_outcomes, minus_pmf)
532
+ joint_dist = symbolic_distribution(joint_outcomes, joint_pmf)
533
+ return dit.shannon.entropy(plus_dist) + dit.shannon.entropy(minus_dist) - dit.shannon.entropy(joint_dist)
534
+ plus_dist = dit.Distribution(plus_outcomes, plus_pmf)
535
+ minus_dist = dit.Distribution(minus_outcomes, minus_pmf)
536
+ joint_dist = dit.Distribution(joint_outcomes, joint_pmf)
537
+ return float(dit.shannon.entropy(plus_dist) + dit.shannon.entropy(minus_dist) - dit.shannon.entropy(joint_dist))
538
+
539
+ def statistical_complexity(self) -> Any:
540
+ """C± = H[S⁺, S⁻] under the bidirectional stationary distribution."""
541
+ dit = _require_dit()
542
+ joint = self.joint_distribution()
543
+ if not joint:
544
+ return 0.0
545
+ outcomes = list(joint.keys())
546
+ probs = [joint[outcome] for outcome in outcomes]
547
+ from sofic.generators.prob import has_symbolic
548
+
549
+ if has_symbolic(probs):
550
+ from dit.symbolic import symbolic_distribution
551
+
552
+ return dit.shannon.entropy(symbolic_distribution(outcomes, probs))
553
+ return float(dit.shannon.entropy(dit.Distribution(outcomes, probs)))
554
+
555
+ def crypticity(self) -> Any:
556
+ """χ = C± − E for a bidirectional presentation."""
557
+ return self.statistical_complexity() - self.excess_entropy()
558
+
559
+ def minimal_generative_model(self, **kwargs: Any) -> Any:
560
+ """Construct the minimal-state-entropy generative presentation."""
561
+ from sofic.generators.minimal_generative_model import minimal_generative_model
562
+
563
+ return minimal_generative_model(self, **kwargs)
564
+
565
+ def wyner_generative_model(self, **kwargs: Any) -> Any:
566
+ """Construct the Wyner-common-information generative presentation."""
567
+ from sofic.generators.minimal_generative_model import wyner_generative_model
568
+
569
+ return wyner_generative_model(self, **kwargs)
570
+
571
+ def functional_generative_model(self, **kwargs: Any) -> Any:
572
+ """Construct the functional-common-information generative presentation."""
573
+ from sofic.generators.minimal_generative_model import functional_generative_model
574
+
575
+ return functional_generative_model(self, **kwargs)
576
+
577
+ def gacs_korner_generative_model(self, **kwargs: Any) -> Any:
578
+ """Construct the Gács-Körner (deterministic meet) generative presentation."""
579
+ from sofic.generators.minimal_generative_model import gacs_korner_generative_model
580
+
581
+ return gacs_korner_generative_model(self, **kwargs)
582
+
583
+ def generative_complexity(self, **kwargs: Any) -> float:
584
+ """C_g = H[G] for the minimal generative model."""
585
+ return self.minimal_generative_model(**kwargs).generative_complexity()
586
+
587
+ def information_anatomy(self) -> dict[str, Any]:
588
+ """Return ρ_μ, b_μ, r_μ, h_μ, E, χ, and the structural/gauge refinement.
589
+
590
+ The ``*_structural`` / ``*_gauge`` keys split the ephemeral (r_μ) and
591
+ bound (b_μ) rates along whether the randomness changes the next forward
592
+ causal state (structural) or merely relabels the output on a fixed
593
+ transition (gauge); each pair sums to its parent.
594
+
595
+ See :meth:`five_variable_anatomy` for the finer four-atom ephemeral
596
+ partition and the reverse-time bound mirror over the full joint
597
+ ``Pr(S⁺₀, S⁻₀, X₀, S⁺₁, S⁻₁)``.
598
+ """
599
+ h_mu = self.entropy_rate()
600
+ return {
601
+ "rho_mu": self.predicted_information(),
602
+ "bound_mu": self.bound_information(),
603
+ "ephemeral_mu": self.ephemeral_information(),
604
+ "ephemeral_structural": self.structural_ephemeral_information(),
605
+ "ephemeral_gauge": self.parallel_edge_information(),
606
+ "bound_structural": self.bound_structural_information(),
607
+ "bound_gauge": self.bound_parallel_edge_information(),
608
+ "entropy_rate": h_mu,
609
+ "excess_entropy": self.excess_entropy(),
610
+ "crypticity": self.crypticity(),
611
+ }
612
+
613
+ def five_variable_anatomy(self) -> dict[str, Any]:
614
+ """Full five-variable anatomy over ``Pr(S⁺₀, S⁻₀, X₀, S⁺₁, S⁻₁)``.
615
+
616
+ Extends :meth:`information_anatomy` with the two unifilarity relations
617
+ (forward ``S⁺₁ = φ⁺(S⁺₀, X₀)`` and reverse ``S⁻₀ = φ⁻(S⁻₁, X₀)``) that let
618
+ both next-states be read off the present. This yields:
619
+
620
+ * the four-atom ephemeral partition
621
+ ``r_μ = r_μ^fwd + r_μ^rev + r_μ^joint + r_μ^gauge`` (keys
622
+ ``ephemeral_forward``, ``ephemeral_reverse``, ``ephemeral_joint``,
623
+ ``ephemeral_pure_gauge``), all ≥ 0, refining the coarse
624
+ structural/gauge split (``r_μ^struct = r_μ^fwd + r_μ^joint``,
625
+ ``r_μ^par = r_μ^rev + r_μ^gauge``);
626
+ * the reverse structural ephemeral rate
627
+ ``r̄_μ^struct = r_μ^rev + r_μ^joint`` (key
628
+ ``ephemeral_structural_reverse``);
629
+ * the reverse-time bound mirror ``b̄_μ``, ``b̄_μ^struct`` and the
630
+ Theorem-A′ gauge residual ``b̄_μ^gauge`` (≈ 0);
631
+ * the internal (causal-state) Markov-chain entropy rates
632
+ ``h_μ^imc = H[S⁺₁ | S⁺₀]`` (key ``internal_markov_rate``) and its
633
+ reverse ``H[S⁻₀ | S⁻₁]`` (key ``internal_markov_rate_reverse``), whose
634
+ difference ``r_μ^fwd − r_μ^rev`` is an arrow-of-time diagnostic.
635
+
636
+ These atoms map onto the fourteen measures of the prediction taxonomy of
637
+ :cite:`jurgens2026taxonomy`; the structural/gauge (unifilarity) reading is
638
+ the refinement layered on top.
639
+ """
640
+ anatomy = self.information_anatomy()
641
+ anatomy.update(
642
+ {
643
+ "ephemeral_forward": self.forward_only_structural_ephemeral(),
644
+ "ephemeral_reverse": self.reverse_only_structural_ephemeral(),
645
+ "ephemeral_joint": self.joint_structural_ephemeral(),
646
+ "ephemeral_pure_gauge": self.pure_gauge_information(),
647
+ "ephemeral_structural_reverse": self.reverse_structural_ephemeral_information(),
648
+ "bound_reverse": self.reverse_bound_information(),
649
+ "bound_structural_reverse": self.reverse_bound_structural_information(),
650
+ "bound_gauge_reverse": self.reverse_bound_gauge_information(),
651
+ "internal_markov_rate": self.internal_markov_entropy_rate(),
652
+ "internal_markov_rate_reverse": self.reverse_internal_markov_entropy_rate(),
653
+ }
654
+ )
655
+ return anatomy
656
+
657
+ def information_diagram(
658
+ self,
659
+ *,
660
+ show_zero: bool = False,
661
+ atoms: str | None = None,
662
+ tol: float = 1e-9,
663
+ ) -> Any:
664
+ """The five-variable information-anatomy I-diagram over the step joint.
665
+
666
+ Returns an :class:`~sofic.generators.information_diagram.InformationDiagram`:
667
+ the ``2⁵ − 1 = 31`` signed I-measure atoms of ``Pr(S⁺₀, S⁻₀, X₀, S⁺₁, S⁻₁)``
668
+ (:cite:`yeung1991new`), each classified into an anatomy role and laid out
669
+ in a fixed order, with the named totals ``r_μ``, ``b⁺_μ``, ``b⁻_μ``,
670
+ ``q_μ``, ``σ_μ``, ``χ⁺``, ``χ⁻``. Pass ``atoms="generic"`` to retain the
671
+ 21 generically nonzero membership sets (Table II's 14 plus 7 cancelling
672
+ extras), or ``atoms="all"`` for every Yeung atom. See
673
+ :func:`sofic.viz.plot_information_diagram` to draw it.
674
+ """
675
+ from sofic.generators.information_diagram import information_diagram
676
+
677
+ return information_diagram(self, show_zero=show_zero, atoms=atoms, tol=tol)
678
+
679
+ def plot_information_diagram(self, **kwargs: Any) -> Any:
680
+ """Draw :meth:`information_diagram` as a colour-coded UpSet plot.
681
+
682
+ Thin wrapper over :func:`sofic.viz.plot_information_diagram`; keyword
683
+ arguments (``atoms``, ``show_zero``, ``role_colors``, ``annotate``,
684
+ ``title``, ``figsize``) are forwarded. Requires the optional
685
+ ``sofic[viz]`` extra.
686
+ """
687
+ from sofic.viz.idiagram import plot_information_diagram
688
+
689
+ return plot_information_diagram(self, **kwargs)