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,668 @@
1
+ """James et al. (2011) block-convergence / anatomy-of-a-bit curves for epsilon-machines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any, Literal
8
+
9
+ import numpy as np
10
+
11
+ from sofic.generators._word_measures import (
12
+ _block_caekl,
13
+ _block_coinformation,
14
+ _block_residual_entropy,
15
+ _block_total_correlation,
16
+ _block_word_distribution,
17
+ )
18
+ from sofic.generators.block_entropy import (
19
+ CMExtensionEstimates,
20
+ _block_entropy_curves,
21
+ _cm_extension_curves,
22
+ _entropy,
23
+ _entropy_rate,
24
+ _entropy_rate_estimates,
25
+ _estimated_excess_entropy,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from sofic.generators.epsilon_machine import EpsilonMachine
30
+
31
+ _TOL = 1e-15
32
+ _CAEKL_RATE_STABLE_STEPS = 2
33
+ _CAEKL_RATE_TOL = 1e-9
34
+ FigureName = Literal["all", "fig4", "fig5", "fig5_rb", "fig5_qw", "fig6", "caekl", "cm"]
35
+
36
+
37
+ def block_caekl(machine: EpsilonMachine, length: int) -> float:
38
+ """Exact block CAEKL mutual information ``J(ℓ)`` for block length ``length``.
39
+
40
+ Uses the ε-machine word distribution ``P(X_{0:ℓ-1})`` and
41
+ ``dit.multivariate.caekl_mutual_information``. Returns ``0`` for ``length <= 1``.
42
+ """
43
+ if length < 0:
44
+ raise ValueError("length must be nonnegative")
45
+ if length <= 1:
46
+ return 0.0
47
+ dist = _block_word_distribution(machine, length)
48
+ return _block_caekl(dist, length)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class CurveConvergence:
53
+ """Rate/intercept/asymptote arrays for one block curve."""
54
+
55
+ rate_estimate: np.ndarray
56
+ intercept: np.ndarray
57
+ asymptote: np.ndarray
58
+ rate: float
59
+ intercept_scalar: float
60
+
61
+
62
+ def _promoted_rate(curve: np.ndarray, *, exact: float | None = None) -> float:
63
+ if exact is not None:
64
+ return float(exact)
65
+ if len(curve) < 2:
66
+ return math.nan
67
+ estimates = _entropy_rate_estimates(curve)
68
+ return float(estimates[-1])
69
+
70
+
71
+ def _convergence_scalars(
72
+ lengths: np.ndarray,
73
+ curve: np.ndarray,
74
+ *,
75
+ rate: float | None = None,
76
+ ) -> CurveConvergence:
77
+ rate_estimate = _entropy_rate_estimates(curve)
78
+ promoted_rate = _promoted_rate(curve, exact=rate)
79
+ intercept = np.array(curve, dtype=float, copy=True)
80
+ if math.isfinite(promoted_rate):
81
+ intercept = curve - promoted_rate * lengths
82
+ intercept_scalar = float(intercept[-1]) if intercept.size else math.nan
83
+ asymptote = curve.copy()
84
+ if math.isfinite(promoted_rate):
85
+ asymptote = intercept_scalar + promoted_rate * lengths
86
+ return CurveConvergence(
87
+ rate_estimate=rate_estimate,
88
+ intercept=intercept,
89
+ asymptote=asymptote,
90
+ rate=promoted_rate,
91
+ intercept_scalar=intercept_scalar,
92
+ )
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class CaeklConvergence(CurveConvergence):
97
+ """CAEKL block-curve convergence with an affine-tail certification flag."""
98
+
99
+ converged: bool
100
+
101
+
102
+ def _caekl_convergence_scalars(
103
+ lengths: np.ndarray,
104
+ block_caekl: np.ndarray,
105
+ *,
106
+ stable_steps: int = _CAEKL_RATE_STABLE_STEPS,
107
+ tol: float = _CAEKL_RATE_TOL,
108
+ ) -> CaeklConvergence:
109
+ """Promote ``j_μ`` from ``J(ℓ)``, certifying exact rate when ``ΔJ`` stabilizes."""
110
+ rate_estimate = _entropy_rate_estimates(block_caekl)
111
+ converged = False
112
+ promoted_rate = math.nan
113
+
114
+ if len(block_caekl) > 2 and stable_steps >= 1:
115
+ diffs = rate_estimate[2:]
116
+ for start in range(len(diffs) - stable_steps + 1):
117
+ window = diffs[start : start + stable_steps]
118
+ if not all(math.isfinite(value) for value in window):
119
+ continue
120
+ reference = float(window[0])
121
+ if all(abs(float(value) - reference) <= tol for value in window):
122
+ promoted_rate = reference
123
+ converged = True
124
+ break
125
+
126
+ if not converged:
127
+ promoted_rate = _promoted_rate(block_caekl)
128
+
129
+ intercept = np.array(block_caekl, dtype=float, copy=True)
130
+ if math.isfinite(promoted_rate):
131
+ intercept = block_caekl - promoted_rate * lengths
132
+ intercept_scalar = float(intercept[-1]) if intercept.size else math.nan
133
+ asymptote = block_caekl.copy()
134
+ if math.isfinite(promoted_rate):
135
+ asymptote = intercept_scalar + promoted_rate * lengths
136
+
137
+ return CaeklConvergence(
138
+ rate_estimate=rate_estimate,
139
+ intercept=intercept,
140
+ asymptote=asymptote,
141
+ rate=promoted_rate,
142
+ intercept_scalar=intercept_scalar,
143
+ converged=converged,
144
+ )
145
+
146
+
147
+ def _anatomy_curves(
148
+ machine: EpsilonMachine,
149
+ max_length: int,
150
+ *,
151
+ block_entropy: np.ndarray,
152
+ h1: float,
153
+ max_caekl_length: int | None = None,
154
+ ) -> dict[str, np.ndarray]:
155
+ block_total_correlation = np.zeros(max_length + 1, dtype=float)
156
+ block_residual_entropy = np.zeros(max_length + 1, dtype=float)
157
+ block_binding_information = np.zeros(max_length + 1, dtype=float)
158
+ block_enigmatic_information = np.zeros(max_length + 1, dtype=float)
159
+ block_local_exogenous_information = np.zeros(max_length + 1, dtype=float)
160
+ block_coinformation = np.zeros(max_length + 1, dtype=float)
161
+ caekl_curve = np.zeros(max_length + 1, dtype=float)
162
+
163
+ for length in range(max_length + 1):
164
+ h_l = float(block_entropy[length])
165
+ if length == 0:
166
+ continue
167
+ if length == 1:
168
+ block_total_correlation[length] = 0.0
169
+ block_residual_entropy[length] = h_l
170
+ block_binding_information[length] = 0.0
171
+ block_enigmatic_information[length] = 0.0
172
+ block_local_exogenous_information[length] = 0.0
173
+ block_coinformation[length] = 0.0
174
+ caekl_curve[length] = 0.0
175
+ continue
176
+
177
+ dist = _block_word_distribution(machine, length)
178
+ t_l = _block_total_correlation(dist, length, h_l, h1)
179
+ r_l = _block_residual_entropy(dist, length)
180
+ b_l = h_l - r_l
181
+ block_total_correlation[length] = t_l
182
+ block_residual_entropy[length] = r_l
183
+ block_binding_information[length] = b_l
184
+ block_enigmatic_information[length] = t_l - b_l
185
+ block_local_exogenous_information[length] = b_l + t_l
186
+ block_coinformation[length] = _block_coinformation(dist, length, h_l)
187
+
188
+ caekl_limit = max_length if max_caekl_length is None else min(max_length, max_caekl_length)
189
+ for length in range(2, caekl_limit + 1):
190
+ caekl_curve[length] = block_caekl(machine, length)
191
+
192
+ return {
193
+ "block_total_correlation": block_total_correlation,
194
+ "block_residual_entropy": block_residual_entropy,
195
+ "block_binding_information": block_binding_information,
196
+ "block_enigmatic_information": block_enigmatic_information,
197
+ "block_local_exogenous_information": block_local_exogenous_information,
198
+ "block_coinformation": block_coinformation,
199
+ "block_caekl": caekl_curve,
200
+ }
201
+
202
+
203
+ def _exact_anatomy_scalars(machine: EpsilonMachine) -> dict[str, float] | None:
204
+ try:
205
+ bidir = machine.to_bidirectional()
206
+ except Exception:
207
+ return None
208
+ h_mu = float(bidir.entropy_rate())
209
+ rho_mu = float(bidir.predicted_information())
210
+ b_mu = float(bidir.bound_information())
211
+ r_mu = float(bidir.ephemeral_information())
212
+ if abs(b_mu + r_mu - h_mu) > 1e-9:
213
+ return None
214
+ q_mu = rho_mu - b_mu
215
+ w_mu = h_mu + rho_mu
216
+ excess = float(bidir.excess_entropy())
217
+ return {
218
+ "entropy_rate": h_mu,
219
+ "predicted_information": rho_mu,
220
+ "bound_information": b_mu,
221
+ "ephemeral_information": r_mu,
222
+ "q_mu": q_mu,
223
+ "w_mu": w_mu,
224
+ "excess_entropy": excess,
225
+ }
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class BlockConvergenceDiagram:
230
+ """James (2011) block convergence curves for an epsilon-machine."""
231
+
232
+ lengths: np.ndarray
233
+ block_entropy: np.ndarray
234
+ block_total_correlation: np.ndarray
235
+ block_residual_entropy: np.ndarray
236
+ block_binding_information: np.ndarray
237
+ block_enigmatic_information: np.ndarray
238
+ block_local_exogenous_information: np.ndarray
239
+ block_coinformation: np.ndarray
240
+ block_caekl: np.ndarray
241
+ state_block_entropy: np.ndarray
242
+ block_state_entropy: np.ndarray
243
+ crypticity_estimate: np.ndarray
244
+ entropy_asymptote: np.ndarray
245
+ entropy_rate_estimate: np.ndarray
246
+ total_correlation_rate_estimate: np.ndarray
247
+ total_correlation_intercept: np.ndarray
248
+ total_correlation_asymptote: np.ndarray
249
+ residual_entropy_rate_estimate: np.ndarray
250
+ residual_entropy_intercept: np.ndarray
251
+ residual_entropy_asymptote: np.ndarray
252
+ binding_information_rate_estimate: np.ndarray
253
+ binding_information_intercept: np.ndarray
254
+ binding_information_asymptote: np.ndarray
255
+ enigmatic_information_rate_estimate: np.ndarray
256
+ enigmatic_information_intercept: np.ndarray
257
+ enigmatic_information_asymptote: np.ndarray
258
+ local_exogenous_information_rate_estimate: np.ndarray
259
+ local_exogenous_information_intercept: np.ndarray
260
+ local_exogenous_information_asymptote: np.ndarray
261
+ coinformation_rate_estimate: np.ndarray
262
+ coinformation_intercept: np.ndarray
263
+ coinformation_asymptote: np.ndarray
264
+ caekl_rate_estimate: np.ndarray
265
+ caekl_intercept: np.ndarray
266
+ caekl_asymptote: np.ndarray
267
+ entropy_rate: float
268
+ predicted_information: float
269
+ ephemeral_information: float
270
+ bound_information: float
271
+ q_mu: float
272
+ w_mu: float
273
+ excess_entropy: float
274
+ E_B: float
275
+ E_R: float
276
+ E_Q: float
277
+ E_W: float
278
+ caekl_rate: float
279
+ caekl_intercept_scalar: float
280
+ caekl_rate_converged: bool
281
+ statistical_complexity: float
282
+ crypticity: float
283
+ markov_order: int | float
284
+ cryptic_order: int | float
285
+
286
+ @property
287
+ def h_mu(self) -> float:
288
+ return self.entropy_rate
289
+
290
+ @property
291
+ def rho_mu(self) -> float:
292
+ return self.predicted_information
293
+
294
+ @property
295
+ def r_mu(self) -> float:
296
+ return self.ephemeral_information
297
+
298
+ @property
299
+ def b_mu(self) -> float:
300
+ return self.bound_information
301
+
302
+ @property
303
+ def E(self) -> float:
304
+ return self.excess_entropy
305
+
306
+ @property
307
+ def j_mu(self) -> float:
308
+ return self.caekl_rate
309
+
310
+ @property
311
+ def J_inf(self) -> float:
312
+ return self.caekl_intercept_scalar
313
+
314
+ def validate_identities(self, *, tol: float = 1e-9) -> None:
315
+ """Assert James (2011) block identities at each finite length."""
316
+ h1 = float(self.block_entropy[1]) if len(self.block_entropy) > 1 else math.nan
317
+ for length in self.lengths:
318
+ index = int(length)
319
+ h_l = float(self.block_entropy[index])
320
+ t_l = float(self.block_total_correlation[index])
321
+ r_l = float(self.block_residual_entropy[index])
322
+ b_l = float(self.block_binding_information[index])
323
+ q_l = float(self.block_enigmatic_information[index])
324
+ w_l = float(self.block_local_exogenous_information[index])
325
+ if index == 0:
326
+ continue
327
+ assert abs(h_l - (b_l + r_l)) <= tol, (index, h_l, b_l, r_l)
328
+ assert abs(t_l - (b_l + q_l)) <= tol, (index, t_l, b_l, q_l)
329
+ assert abs(w_l - (b_l + t_l)) <= tol, (index, w_l, b_l, t_l)
330
+ if index >= 1 and math.isfinite(h1):
331
+ assert abs(h_l + t_l - index * h1) <= tol, (index, h_l, t_l, h1)
332
+ assert abs(r_l + w_l - index * h1) <= tol, (index, r_l, w_l, h1)
333
+
334
+ def plot(
335
+ self,
336
+ ax: Any | None = None,
337
+ *,
338
+ figure: FigureName = "all",
339
+ show_asymptotes: bool = True,
340
+ show_grid: bool = True,
341
+ show_legend: bool = True,
342
+ title: str | None = None,
343
+ marker: str = "o",
344
+ ) -> Any:
345
+ if ax is None:
346
+ import matplotlib.pyplot as plt
347
+
348
+ if figure == "all":
349
+ _, axes = plt.subplots(2, 3, figsize=(12, 7))
350
+ axes_list = axes.ravel()
351
+ for panel, name in zip(
352
+ axes_list,
353
+ ("fig4", "fig5_rb", "fig5_qw", "fig6", "caekl", "cm"),
354
+ strict=False,
355
+ ):
356
+ if name == "cm":
357
+ self._plot_cm(panel, marker=marker, show_grid=show_grid, show_legend=show_legend)
358
+ else:
359
+ self.plot(
360
+ ax=panel,
361
+ figure=name,
362
+ show_asymptotes=show_asymptotes,
363
+ show_grid=show_grid,
364
+ show_legend=show_legend,
365
+ )
366
+ if title is not None:
367
+ axes_list[0].figure.suptitle(title)
368
+ return axes_list
369
+ _, ax = plt.subplots()
370
+
371
+ if figure == "fig4":
372
+ ax.plot(self.lengths, self.block_entropy, marker=marker, label=r"$H(\ell)$")
373
+ ax.plot(self.lengths, self.block_total_correlation, marker=marker, label=r"$T(\ell)$")
374
+ if show_asymptotes:
375
+ ax.plot(
376
+ self.lengths,
377
+ self.entropy_asymptote,
378
+ linestyle="--",
379
+ color="black",
380
+ label=r"$E + h_\mu \ell$",
381
+ )
382
+ ax.plot(
383
+ self.lengths,
384
+ self.total_correlation_asymptote,
385
+ linestyle="--",
386
+ color="tab:orange",
387
+ label=r"$-E + \rho_\mu \ell$",
388
+ )
389
+ elif figure == "fig5":
390
+ ax.plot(self.lengths, self.block_residual_entropy, marker=marker, label=r"$R(\ell)$")
391
+ ax.plot(self.lengths, self.block_binding_information, marker=marker, label=r"$B(\ell)$")
392
+ ax.plot(self.lengths, self.block_enigmatic_information, marker=marker, label=r"$Q(\ell)$")
393
+ ax.plot(self.lengths, self.block_local_exogenous_information, marker=marker, label=r"$W(\ell)$")
394
+ if show_asymptotes:
395
+ ax.plot(self.lengths, self.residual_entropy_asymptote, linestyle="--", label=r"$E_R + r_\mu \ell$")
396
+ ax.plot(self.lengths, self.binding_information_asymptote, linestyle="--", label=r"$E_B + b_\mu \ell$")
397
+ ax.plot(self.lengths, self.enigmatic_information_asymptote, linestyle="--", label=r"$E_Q + q_\mu \ell$")
398
+ ax.plot(
399
+ self.lengths,
400
+ self.local_exogenous_information_asymptote,
401
+ linestyle="--",
402
+ label=r"$E_W + w_\mu \ell$",
403
+ )
404
+ elif figure == "fig5_rb":
405
+ ax.plot(self.lengths, self.block_residual_entropy, marker=marker, label=r"$R(\ell)$")
406
+ ax.plot(self.lengths, self.block_binding_information, marker=marker, label=r"$B(\ell)$")
407
+ if show_asymptotes:
408
+ ax.plot(self.lengths, self.residual_entropy_asymptote, linestyle="--", label=r"$E_R + r_\mu \ell$")
409
+ ax.plot(self.lengths, self.binding_information_asymptote, linestyle="--", label=r"$E_B + b_\mu \ell$")
410
+ elif figure == "fig5_qw":
411
+ ax.plot(self.lengths, self.block_enigmatic_information, marker=marker, label=r"$Q(\ell)$")
412
+ ax.plot(self.lengths, self.block_local_exogenous_information, marker=marker, label=r"$W(\ell)$")
413
+ if show_asymptotes:
414
+ ax.plot(self.lengths, self.enigmatic_information_asymptote, linestyle="--", label=r"$E_Q + q_\mu \ell$")
415
+ ax.plot(
416
+ self.lengths,
417
+ self.local_exogenous_information_asymptote,
418
+ linestyle="--",
419
+ label=r"$E_W + w_\mu \ell$",
420
+ )
421
+ elif figure == "fig6":
422
+ ax.plot(self.lengths, self.block_coinformation, marker=marker, label=r"$I(\ell)$")
423
+ if show_asymptotes:
424
+ ax.plot(self.lengths, self.coinformation_asymptote, linestyle="--", label=r"$I + i_\mu \ell$")
425
+ elif figure == "caekl":
426
+ ax.plot(self.lengths, self.block_caekl, marker=marker, label=r"$J(\ell)$")
427
+ if show_asymptotes:
428
+ ax.plot(self.lengths, self.caekl_asymptote, linestyle="--", label=r"$J_\infty + j_\mu \ell$")
429
+ elif figure == "cm":
430
+ self._plot_cm(ax, marker=marker, show_grid=False, show_legend=show_legend, show_asymptotes=show_asymptotes)
431
+ else:
432
+ raise ValueError(f"unknown figure {figure!r}")
433
+
434
+ if show_grid:
435
+ ax.set_axisbelow(True)
436
+ ax.grid(True, color="0.85", linewidth=0.8)
437
+ ax.set_xlabel(r"Block length $\ell$")
438
+ ax.set_ylabel("Information (bits)")
439
+ if title is not None:
440
+ ax.set_title(title)
441
+ if show_legend:
442
+ ax.legend()
443
+ return ax
444
+
445
+ def _plot_cm(
446
+ self,
447
+ ax: Any,
448
+ *,
449
+ marker: str,
450
+ show_grid: bool,
451
+ show_legend: bool,
452
+ show_asymptotes: bool = True,
453
+ ) -> None:
454
+ ax.plot(self.lengths, self.block_entropy, marker=marker, label=r"$H[X_{0:\ell}]$")
455
+ ax.plot(self.lengths, self.state_block_entropy, marker=marker, label=r"$H[S_0, X_{0:\ell}]$")
456
+ ax.plot(self.lengths, self.block_state_entropy, marker=marker, label=r"$H[X_{0:\ell}, S_\ell]$")
457
+ ax.plot(self.lengths, self.crypticity_estimate, marker=marker, linestyle="-.", label=r"$\chi(\ell)$")
458
+ if show_asymptotes:
459
+ ax.plot(self.lengths, self.entropy_asymptote, linestyle="--", color="black", label=r"$E + h_\mu \ell$")
460
+ if show_grid:
461
+ ax.set_axisbelow(True)
462
+ ax.grid(True, color="0.85", linewidth=0.8)
463
+ ax.set_xlabel(r"Block length $\ell$")
464
+ ax.set_ylabel("Information (bits)")
465
+ if show_legend:
466
+ ax.legend()
467
+
468
+
469
+ @dataclass(frozen=True)
470
+ class BlockConvergenceEstimates(BlockConvergenceDiagram, CMExtensionEstimates):
471
+ """Block convergence diagram plus Ellison/Mahoney CM extension curves."""
472
+
473
+ def information_anatomy(self) -> dict[str, float]:
474
+ """Return promoted anatomy scalars using James (2011) key names."""
475
+ return {
476
+ "rho_mu": self.predicted_information,
477
+ "bound_mu": self.bound_information,
478
+ "ephemeral_mu": self.ephemeral_information,
479
+ "entropy_rate": self.entropy_rate,
480
+ "excess_entropy": self.excess_entropy,
481
+ "crypticity": self.crypticity,
482
+ "q_mu": self.q_mu,
483
+ "w_mu": self.w_mu,
484
+ "E_B": self.E_B,
485
+ "E_R": self.E_R,
486
+ "E_Q": self.E_Q,
487
+ "E_W": self.E_W,
488
+ "j_mu": self.caekl_rate,
489
+ "caekl_intercept": self.caekl_intercept_scalar,
490
+ "caekl_rate_converged": self.caekl_rate_converged,
491
+ }
492
+
493
+
494
+ def block_convergence_diagram(
495
+ machine: EpsilonMachine,
496
+ max_length: int,
497
+ *,
498
+ max_caekl_length: int | None = None,
499
+ ) -> BlockConvergenceDiagram:
500
+ estimates = block_convergence_estimates(
501
+ machine,
502
+ max_length,
503
+ max_caekl_length=max_caekl_length,
504
+ )
505
+ return BlockConvergenceDiagram(
506
+ **{field: getattr(estimates, field) for field in BlockConvergenceDiagram.__dataclass_fields__}
507
+ )
508
+
509
+
510
+ def block_convergence_estimates(
511
+ machine: EpsilonMachine,
512
+ max_length: int,
513
+ *,
514
+ entropy_rate: float | None = None,
515
+ use_exact: bool = True,
516
+ max_caekl_length: int | None = None,
517
+ ) -> BlockConvergenceEstimates:
518
+ if max_length < 0:
519
+ raise ValueError("max_length must be nonnegative")
520
+ if max_caekl_length is not None and max_caekl_length < 0:
521
+ raise ValueError("max_caekl_length must be nonnegative")
522
+
523
+ lengths, block_entropy, state_block_entropy, block_state_entropy, pi, symbol_matrices = _block_entropy_curves(
524
+ machine,
525
+ max_length,
526
+ )
527
+ h1 = float(block_entropy[1]) if max_length >= 1 else math.nan
528
+ anatomy = _anatomy_curves(
529
+ machine,
530
+ max_length,
531
+ block_entropy=block_entropy,
532
+ h1=h1,
533
+ max_caekl_length=max_caekl_length,
534
+ )
535
+
536
+ exact = _exact_anatomy_scalars(machine) if use_exact else None
537
+ h_mu_exact = exact["entropy_rate"] if exact else None
538
+ h_mu = float(
539
+ entropy_rate
540
+ if entropy_rate is not None
541
+ else (_entropy_rate(pi, symbol_matrices) if h_mu_exact is None else h_mu_exact)
542
+ )
543
+
544
+ statistical_complexity = _entropy(pi)
545
+ excess_entropy = _estimated_excess_entropy(
546
+ machine,
547
+ 0.5 * (block_entropy - h_mu * lengths + block_state_entropy - h_mu * lengths),
548
+ use_exact=use_exact,
549
+ )
550
+ if exact is not None:
551
+ excess_entropy = exact["excess_entropy"]
552
+ crypticity = statistical_complexity - excess_entropy
553
+ entropy_asymptote = excess_entropy + h_mu * lengths
554
+ crypticity_estimate = state_block_entropy - block_state_entropy
555
+ entropy_rate_estimate = _entropy_rate_estimates(block_entropy)
556
+
557
+ t_conv = _convergence_scalars(
558
+ lengths,
559
+ anatomy["block_total_correlation"],
560
+ rate=exact["predicted_information"] if exact else None,
561
+ )
562
+ r_conv = _convergence_scalars(
563
+ lengths,
564
+ anatomy["block_residual_entropy"],
565
+ rate=exact["ephemeral_information"] if exact else None,
566
+ )
567
+ b_conv = _convergence_scalars(
568
+ lengths,
569
+ anatomy["block_binding_information"],
570
+ rate=exact["bound_information"] if exact else None,
571
+ )
572
+ q_conv = _convergence_scalars(lengths, anatomy["block_enigmatic_information"])
573
+ w_conv = _convergence_scalars(lengths, anatomy["block_local_exogenous_information"])
574
+ i_conv = _convergence_scalars(lengths, anatomy["block_coinformation"])
575
+ j_conv = _caekl_convergence_scalars(lengths, anatomy["block_caekl"])
576
+
577
+ rho_mu = exact["predicted_information"] if exact else t_conv.rate
578
+ # James block binding B(ℓ) = H(ℓ) − R(ℓ) → b_μ; residual R(ℓ) → r_μ.
579
+ b_mu = exact["bound_information"] if exact else b_conv.rate
580
+ r_mu = exact["ephemeral_information"] if exact else r_conv.rate
581
+ q_mu = exact["q_mu"] if exact else q_conv.rate
582
+ w_mu = exact["w_mu"] if exact else w_conv.rate
583
+
584
+ cm = _cm_extension_curves(
585
+ lengths,
586
+ block_entropy,
587
+ state_block_entropy,
588
+ block_state_entropy,
589
+ h_mu=h_mu,
590
+ statistical_complexity=statistical_complexity,
591
+ excess_entropy=excess_entropy,
592
+ entropy_asymptote=entropy_asymptote,
593
+ crypticity_estimate=crypticity_estimate,
594
+ )
595
+
596
+ return BlockConvergenceEstimates(
597
+ lengths=lengths,
598
+ block_entropy=block_entropy,
599
+ block_total_correlation=anatomy["block_total_correlation"],
600
+ block_residual_entropy=anatomy["block_residual_entropy"],
601
+ block_binding_information=anatomy["block_binding_information"],
602
+ block_enigmatic_information=anatomy["block_enigmatic_information"],
603
+ block_local_exogenous_information=anatomy["block_local_exogenous_information"],
604
+ block_coinformation=anatomy["block_coinformation"],
605
+ block_caekl=anatomy["block_caekl"],
606
+ state_block_entropy=state_block_entropy,
607
+ block_state_entropy=block_state_entropy,
608
+ crypticity_estimate=crypticity_estimate,
609
+ entropy_asymptote=entropy_asymptote,
610
+ entropy_rate_estimate=entropy_rate_estimate,
611
+ total_correlation_rate_estimate=t_conv.rate_estimate,
612
+ total_correlation_intercept=t_conv.intercept,
613
+ total_correlation_asymptote=t_conv.asymptote,
614
+ residual_entropy_rate_estimate=r_conv.rate_estimate,
615
+ residual_entropy_intercept=r_conv.intercept,
616
+ residual_entropy_asymptote=r_conv.asymptote,
617
+ binding_information_rate_estimate=b_conv.rate_estimate,
618
+ binding_information_intercept=b_conv.intercept,
619
+ binding_information_asymptote=b_conv.asymptote,
620
+ enigmatic_information_rate_estimate=q_conv.rate_estimate,
621
+ enigmatic_information_intercept=q_conv.intercept,
622
+ enigmatic_information_asymptote=q_conv.asymptote,
623
+ local_exogenous_information_rate_estimate=w_conv.rate_estimate,
624
+ local_exogenous_information_intercept=w_conv.intercept,
625
+ local_exogenous_information_asymptote=w_conv.asymptote,
626
+ coinformation_rate_estimate=i_conv.rate_estimate,
627
+ coinformation_intercept=i_conv.intercept,
628
+ coinformation_asymptote=i_conv.asymptote,
629
+ caekl_rate_estimate=j_conv.rate_estimate,
630
+ caekl_intercept=j_conv.intercept,
631
+ caekl_asymptote=j_conv.asymptote,
632
+ entropy_rate=h_mu,
633
+ predicted_information=float(rho_mu),
634
+ ephemeral_information=float(r_mu),
635
+ bound_information=float(b_mu),
636
+ q_mu=float(q_mu),
637
+ w_mu=float(w_mu),
638
+ excess_entropy=float(excess_entropy),
639
+ E_B=float(b_conv.intercept_scalar),
640
+ E_R=float(r_conv.intercept_scalar),
641
+ E_Q=float(q_conv.intercept_scalar),
642
+ E_W=float(w_conv.intercept_scalar),
643
+ caekl_rate=float(j_conv.rate),
644
+ caekl_intercept_scalar=float(j_conv.intercept_scalar),
645
+ caekl_rate_converged=bool(j_conv.converged),
646
+ statistical_complexity=float(statistical_complexity),
647
+ crypticity=float(crypticity),
648
+ markov_order=machine.markov_order(),
649
+ cryptic_order=machine.cryptic_order(),
650
+ excess_entropy_lower=cm.excess_entropy_lower,
651
+ excess_entropy_upper=cm.excess_entropy_upper,
652
+ excess_entropy_estimate=cm.excess_entropy_estimate,
653
+ synchronization_estimate=cm.synchronization_estimate,
654
+ reverse_synchronization_estimate=cm.reverse_synchronization_estimate,
655
+ transient_information_estimate=cm.transient_information_estimate,
656
+ predictability_gain_estimate=cm.predictability_gain_estimate,
657
+ oracular_information_estimate=cm.oracular_information_estimate,
658
+ gauge_information_estimate=cm.gauge_information_estimate,
659
+ )
660
+
661
+
662
+ def plot_block_convergence_diagram(
663
+ machine: EpsilonMachine,
664
+ max_length: int,
665
+ ax: Any | None = None,
666
+ **kwargs: Any,
667
+ ) -> Any:
668
+ return block_convergence_diagram(machine, max_length).plot(ax=ax, **kwargs)