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.
- sofic/__init__.py +185 -0
- sofic/automata/__init__.py +207 -0
- sofic/automata/_config_simulation.py +40 -0
- sofic/automata/active.py +611 -0
- sofic/automata/alergia.py +222 -0
- sofic/automata/algorithms.py +376 -0
- sofic/automata/atomaton.py +58 -0
- sofic/automata/base.py +161 -0
- sofic/automata/buchi.py +23 -0
- sofic/automata/buchi_simulation.py +67 -0
- sofic/automata/canonical_dual.py +18 -0
- sofic/automata/canonical_extraction.py +122 -0
- sofic/automata/dfa.py +85 -0
- sofic/automata/dfasat.py +195 -0
- sofic/automata/edsm.py +219 -0
- sofic/automata/enumeration.py +44 -0
- sofic/automata/icdfa.py +421 -0
- sofic/automata/idfa.py +363 -0
- sofic/automata/languages/__init__.py +39 -0
- sofic/automata/languages/_quotient_utils.py +64 -0
- sofic/automata/languages/atoms.py +31 -0
- sofic/automata/languages/automaton_ops.py +243 -0
- sofic/automata/languages/base.py +67 -0
- sofic/automata/languages/operations.py +78 -0
- sofic/automata/languages/quotients.py +66 -0
- sofic/automata/languages/residuals.py +25 -0
- sofic/automata/learning.py +79 -0
- sofic/automata/nfa.py +39 -0
- sofic/automata/nwa.py +343 -0
- sofic/automata/nwa_simulation.py +56 -0
- sofic/automata/observation.py +40 -0
- sofic/automata/papni.py +301 -0
- sofic/automata/regex.py +128 -0
- sofic/automata/rfsa.py +35 -0
- sofic/automata/rpni.py +193 -0
- sofic/automata/subsequential.py +201 -0
- sofic/automata/transducer_operations.py +350 -0
- sofic/automata/transducer_simulation.py +150 -0
- sofic/automata/transducers.py +365 -0
- sofic/automata/unifilar.py +107 -0
- sofic/automata/vpa.py +1373 -0
- sofic/automata/vpa_simulation.py +53 -0
- sofic/base.py +153 -0
- sofic/core.py +47 -0
- sofic/examples/__init__.py +86 -0
- sofic/examples/epsilon_machines.py +1089 -0
- sofic/examples/processes.py +1491 -0
- sofic/examples/shifts.py +144 -0
- sofic/exceptions.py +33 -0
- sofic/generators/__init__.py +115 -0
- sofic/generators/_word_measures.py +94 -0
- sofic/generators/alternative_complexity.py +104 -0
- sofic/generators/base.py +327 -0
- sofic/generators/bidirectional_construction.py +717 -0
- sofic/generators/bidirectional_epsilon_machine.py +689 -0
- sofic/generators/block_convergence.py +668 -0
- sofic/generators/block_entropy.py +578 -0
- sofic/generators/channel_measures.py +75 -0
- sofic/generators/conversions.py +182 -0
- sofic/generators/directional_flow.py +245 -0
- sofic/generators/edge_emissions.py +36 -0
- sofic/generators/edge_machine.py +178 -0
- sofic/generators/epsilon_construction.py +193 -0
- sofic/generators/epsilon_inference.py +703 -0
- sofic/generators/epsilon_machine.py +557 -0
- sofic/generators/epsilon_transducer.py +168 -0
- sofic/generators/epsilon_transducer_construction.py +185 -0
- sofic/generators/epsilon_transducer_inference.py +499 -0
- sofic/generators/hmm_inference.py +719 -0
- sofic/generators/information_diagram.py +428 -0
- sofic/generators/lumping.py +447 -0
- sofic/generators/markov.py +100 -0
- sofic/generators/mealy.py +156 -0
- sofic/generators/measures.py +257 -0
- sofic/generators/minimal_generative_model.py +821 -0
- sofic/generators/mixed_state.py +250 -0
- sofic/generators/mixed_state_construction.py +163 -0
- sofic/generators/moore.py +75 -0
- sofic/generators/nmachine.py +78 -0
- sofic/generators/nmachine_construction.py +70 -0
- sofic/generators/pfa.py +100 -0
- sofic/generators/prob.py +291 -0
- sofic/generators/process_equivalence.py +207 -0
- sofic/generators/quasi_inference.py +74 -0
- sofic/generators/quasi_realization.py +97 -0
- sofic/generators/reversal.py +66 -0
- sofic/generators/stack_hmm.py +426 -0
- sofic/generators/stack_inference.py +509 -0
- sofic/generators/stationary.py +134 -0
- sofic/generators/stochastic.py +65 -0
- sofic/generators/synchronization.py +407 -0
- sofic/generators/topological_epsilon_enumeration.py +349 -0
- sofic/generators/words.py +226 -0
- sofic/graph.py +135 -0
- sofic/indexing.py +31 -0
- sofic/inference/__init__.py +45 -0
- sofic/inference/bayesian/__init__.py +68 -0
- sofic/inference/bayesian/comparison.py +199 -0
- sofic/inference/bayesian/counts.py +219 -0
- sofic/inference/bayesian/diversity.py +254 -0
- sofic/inference/bayesian/epsilon.py +270 -0
- sofic/inference/bayesian/hdp_hmm.py +340 -0
- sofic/inference/bayesian/markov.py +294 -0
- sofic/inference/bayesian/pymc_backend.py +71 -0
- sofic/inference/bayesian/stack_hmm.py +215 -0
- sofic/inference/model_selection.py +365 -0
- sofic/inference/spectral.py +564 -0
- sofic/operations.py +16 -0
- sofic/properties.py +339 -0
- sofic/serialization.py +450 -0
- sofic/shifts/__init__.py +48 -0
- sofic/shifts/algorithms.py +84 -0
- sofic/shifts/base.py +49 -0
- sofic/shifts/cover_construction.py +76 -0
- sofic/shifts/covers.py +47 -0
- sofic/shifts/dyck_algorithms.py +100 -0
- sofic/shifts/dyck_enumeration.py +275 -0
- sofic/shifts/markov_dyck.py +172 -0
- sofic/shifts/parry_construction.py +82 -0
- sofic/shifts/sft.py +104 -0
- sofic/shifts/sft_construction.py +52 -0
- sofic/shifts/sliding_block_code.py +156 -0
- sofic/shifts/sofic.py +111 -0
- sofic/shifts/sofic_dyck.py +110 -0
- sofic/shifts/sofic_relation.py +64 -0
- sofic/shifts/textile.py +104 -0
- sofic/shifts/tmc.py +46 -0
- sofic/shifts/tmc_construction.py +58 -0
- sofic/shifts/topological_anatomy.py +150 -0
- sofic/states.py +27 -0
- sofic/testing/__init__.py +8 -0
- sofic/testing/strategies.py +154 -0
- sofic/viz/__init__.py +16 -0
- sofic/viz/_context.py +345 -0
- sofic/viz/_edge.py +216 -0
- sofic/viz/_format.py +89 -0
- sofic/viz/_labels.py +34 -0
- sofic/viz/_names.py +17 -0
- sofic/viz/_rational.py +20 -0
- sofic/viz/_tikz_compile.py +177 -0
- sofic/viz/_tikz_format.py +122 -0
- sofic/viz/_tikz_layout.py +218 -0
- sofic/viz/assets/vaucanson.tikz +71 -0
- sofic/viz/graphviz.py +158 -0
- sofic/viz/idiagram.py +350 -0
- sofic/viz/tikz.py +381 -0
- sofic-0.1.0.dist-info/METADATA +444 -0
- sofic-0.1.0.dist-info/RECORD +150 -0
- sofic-0.1.0.dist-info/WHEEL +4 -0
- sofic-0.1.0.dist-info/licenses/LICENSE.txt +29 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""Block entropy convergence diagrams for epsilon-machines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
14
|
+
|
|
15
|
+
_TOL = 1e-15
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class BlockEntropyDiagram:
|
|
20
|
+
"""Finite-block entropy curves and computational-mechanics annotations.
|
|
21
|
+
|
|
22
|
+
The curves follow the entropy-convergence diagrams used for finite-state
|
|
23
|
+
epsilon-machines: block entropy ``H[X_0:L]``, state-block entropy
|
|
24
|
+
``H[S_0, X_0:L]``, block-state entropy ``H[X_0:L, S_L]``, the linear
|
|
25
|
+
asymptote ``E + h_mu L``, and the finite-length crypticity estimate
|
|
26
|
+
``chi(L)``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
lengths: np.ndarray
|
|
30
|
+
block_entropy: np.ndarray
|
|
31
|
+
state_block_entropy: np.ndarray
|
|
32
|
+
block_state_entropy: np.ndarray
|
|
33
|
+
entropy_asymptote: np.ndarray
|
|
34
|
+
crypticity_estimate: np.ndarray
|
|
35
|
+
entropy_rate_estimate: np.ndarray
|
|
36
|
+
entropy_rate: float
|
|
37
|
+
excess_entropy: float
|
|
38
|
+
statistical_complexity: float
|
|
39
|
+
crypticity: float
|
|
40
|
+
markov_order: int | float
|
|
41
|
+
cryptic_order: int | float
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def transient_information(self) -> float:
|
|
45
|
+
"""Transient information ``T`` (Crutchfield & Feldman, 2003).
|
|
46
|
+
|
|
47
|
+
Equal to the accumulated area between the block-entropy curve and its
|
|
48
|
+
linear asymptote over the plotted block lengths,
|
|
49
|
+
``sum_L (E + h_mu L - H[X_{0:L}])``.
|
|
50
|
+
"""
|
|
51
|
+
return float(np.sum(self.entropy_asymptote - self.block_entropy))
|
|
52
|
+
|
|
53
|
+
def plot(
|
|
54
|
+
self,
|
|
55
|
+
ax: Any | None = None,
|
|
56
|
+
*,
|
|
57
|
+
show_block_entropy: bool = True,
|
|
58
|
+
show_state_block_entropy: bool = False,
|
|
59
|
+
show_block_state_entropy: bool = True,
|
|
60
|
+
show_asymptote: bool = True,
|
|
61
|
+
show_transient_information: bool = True,
|
|
62
|
+
show_markov_order: bool = True,
|
|
63
|
+
show_cryptic_order: bool = True,
|
|
64
|
+
show_crypticity: bool = False,
|
|
65
|
+
show_excess_entropy: bool = False,
|
|
66
|
+
show_statistical_complexity: bool = False,
|
|
67
|
+
show_entropy_rate_estimate: bool = False,
|
|
68
|
+
show_grid: bool = True,
|
|
69
|
+
show_legend: bool = True,
|
|
70
|
+
title: str | None = None,
|
|
71
|
+
marker: str = "o",
|
|
72
|
+
) -> Any:
|
|
73
|
+
"""Plot selected block entropy diagram features on ``ax``.
|
|
74
|
+
|
|
75
|
+
``matplotlib`` is imported only when this method is called. The return
|
|
76
|
+
value is the axes object used for plotting.
|
|
77
|
+
"""
|
|
78
|
+
if ax is None:
|
|
79
|
+
import matplotlib.pyplot as plt
|
|
80
|
+
|
|
81
|
+
_, ax = plt.subplots()
|
|
82
|
+
|
|
83
|
+
if show_transient_information and show_block_entropy and show_asymptote:
|
|
84
|
+
# Shade the transient information: the area between H[X_{0:L}] and the
|
|
85
|
+
# E + h_mu L asymptote. Drawn first (low zorder) so lines stay on top.
|
|
86
|
+
ax.fill_between(
|
|
87
|
+
self.lengths,
|
|
88
|
+
self.block_entropy,
|
|
89
|
+
self.entropy_asymptote,
|
|
90
|
+
color="tab:blue",
|
|
91
|
+
alpha=0.15,
|
|
92
|
+
zorder=0,
|
|
93
|
+
label=rf"$\mathbf{{T}} = {self.transient_information:.3f}$",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if show_block_entropy:
|
|
97
|
+
ax.plot(self.lengths, self.block_entropy, marker=marker, label=r"$H[X_{0:L}]$")
|
|
98
|
+
if show_state_block_entropy:
|
|
99
|
+
ax.plot(
|
|
100
|
+
self.lengths,
|
|
101
|
+
self.state_block_entropy,
|
|
102
|
+
marker=marker,
|
|
103
|
+
label=r"$H[S_0, X_{0:L}]$",
|
|
104
|
+
)
|
|
105
|
+
if show_block_state_entropy:
|
|
106
|
+
ax.plot(
|
|
107
|
+
self.lengths,
|
|
108
|
+
self.block_state_entropy,
|
|
109
|
+
marker=marker,
|
|
110
|
+
label=r"$H[X_{0:L}, S_L]$",
|
|
111
|
+
)
|
|
112
|
+
if show_asymptote:
|
|
113
|
+
ax.plot(
|
|
114
|
+
self.lengths,
|
|
115
|
+
self.entropy_asymptote,
|
|
116
|
+
linestyle="--",
|
|
117
|
+
color="black",
|
|
118
|
+
label=r"$E + h_\mu L$",
|
|
119
|
+
)
|
|
120
|
+
if show_crypticity:
|
|
121
|
+
ax.plot(
|
|
122
|
+
self.lengths,
|
|
123
|
+
self.crypticity_estimate,
|
|
124
|
+
marker=marker,
|
|
125
|
+
linestyle="-.",
|
|
126
|
+
label=r"$\chi(L)$",
|
|
127
|
+
)
|
|
128
|
+
ax.axhline(
|
|
129
|
+
self.crypticity,
|
|
130
|
+
linestyle=":",
|
|
131
|
+
color="tab:purple",
|
|
132
|
+
label=r"$\chi$",
|
|
133
|
+
)
|
|
134
|
+
if show_excess_entropy:
|
|
135
|
+
ax.axhline(self.excess_entropy, linestyle=":", color="tab:green", label=r"$E$")
|
|
136
|
+
if show_statistical_complexity:
|
|
137
|
+
ax.axhline(
|
|
138
|
+
self.statistical_complexity,
|
|
139
|
+
linestyle=":",
|
|
140
|
+
color="tab:brown",
|
|
141
|
+
label=r"$C_\mu$",
|
|
142
|
+
)
|
|
143
|
+
if show_entropy_rate_estimate:
|
|
144
|
+
ax.plot(
|
|
145
|
+
self.lengths,
|
|
146
|
+
self.entropy_rate_estimate,
|
|
147
|
+
marker=marker,
|
|
148
|
+
linestyle=":",
|
|
149
|
+
label=r"$h_\mu(L)$",
|
|
150
|
+
)
|
|
151
|
+
ax.axhline(self.entropy_rate, linestyle="--", color="tab:gray", label=r"$h_\mu$")
|
|
152
|
+
|
|
153
|
+
if show_markov_order:
|
|
154
|
+
self._plot_order_line(ax, self.markov_order, "tab:red", r"$R$")
|
|
155
|
+
if show_cryptic_order:
|
|
156
|
+
self._plot_order_line(ax, self.cryptic_order, "tab:orange", r"$k_\chi$")
|
|
157
|
+
|
|
158
|
+
if show_grid:
|
|
159
|
+
ax.set_axisbelow(True)
|
|
160
|
+
ax.grid(True, color="0.85", linewidth=0.8)
|
|
161
|
+
else:
|
|
162
|
+
ax.grid(False)
|
|
163
|
+
ax.set_xlabel("Block length L")
|
|
164
|
+
ax.set_ylabel("Information (bits)")
|
|
165
|
+
if title is not None:
|
|
166
|
+
ax.set_title(title)
|
|
167
|
+
if show_legend:
|
|
168
|
+
ax.legend()
|
|
169
|
+
return ax
|
|
170
|
+
|
|
171
|
+
def _plot_order_line(self, ax: Any, order: int | float, color: str, symbol: str) -> None:
|
|
172
|
+
if not _is_finite_order(order):
|
|
173
|
+
return
|
|
174
|
+
if float(order) < float(self.lengths[0]) or float(order) > float(self.lengths[-1]):
|
|
175
|
+
return
|
|
176
|
+
label = f"{symbol} = {int(order)}"
|
|
177
|
+
ax.axvline(float(order), linestyle="--", color=color, alpha=0.75, label=label)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class CMExtensionEstimates:
|
|
182
|
+
"""Ellison/Mahoney CM-extension curves shared by block estimate diagrams.
|
|
183
|
+
|
|
184
|
+
These nine finite-block curves are computed identically for
|
|
185
|
+
:class:`BlockEntropyEstimates` and
|
|
186
|
+
:class:`~sofic.generators.block_convergence.BlockConvergenceEstimates`
|
|
187
|
+
(see :func:`_cm_extension_curves`).
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
excess_entropy_lower: np.ndarray
|
|
191
|
+
excess_entropy_upper: np.ndarray
|
|
192
|
+
excess_entropy_estimate: np.ndarray
|
|
193
|
+
synchronization_estimate: np.ndarray
|
|
194
|
+
reverse_synchronization_estimate: np.ndarray
|
|
195
|
+
transient_information_estimate: np.ndarray
|
|
196
|
+
predictability_gain_estimate: np.ndarray
|
|
197
|
+
oracular_information_estimate: np.ndarray
|
|
198
|
+
gauge_information_estimate: np.ndarray
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(frozen=True)
|
|
202
|
+
class BlockEntropyEstimates(CMExtensionEstimates):
|
|
203
|
+
"""Finite-block estimates for computational-mechanics quantities."""
|
|
204
|
+
|
|
205
|
+
lengths: np.ndarray
|
|
206
|
+
block_entropy: np.ndarray
|
|
207
|
+
state_block_entropy: np.ndarray
|
|
208
|
+
block_state_entropy: np.ndarray
|
|
209
|
+
entropy_asymptote: np.ndarray
|
|
210
|
+
entropy_rate_estimate: np.ndarray
|
|
211
|
+
residual_entropy: np.ndarray
|
|
212
|
+
residual_entropy_rate_estimate: np.ndarray
|
|
213
|
+
crypticity_estimate: np.ndarray
|
|
214
|
+
entropy_rate: float
|
|
215
|
+
excess_entropy: float
|
|
216
|
+
statistical_complexity: float
|
|
217
|
+
crypticity: float
|
|
218
|
+
predicted_information: float
|
|
219
|
+
bound_information: float
|
|
220
|
+
ephemeral_information: float
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def h_mu(self) -> float:
|
|
224
|
+
return self.entropy_rate
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def E(self) -> float:
|
|
228
|
+
return self.excess_entropy
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def r_mu(self) -> float:
|
|
232
|
+
return self.ephemeral_information
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def b_mu(self) -> float:
|
|
236
|
+
return self.bound_information
|
|
237
|
+
|
|
238
|
+
def information_anatomy(self) -> dict[str, float]:
|
|
239
|
+
"""Return finite-block estimates using the exact anatomy key names."""
|
|
240
|
+
return {
|
|
241
|
+
"rho_mu": self.predicted_information,
|
|
242
|
+
"bound_mu": self.bound_information,
|
|
243
|
+
"ephemeral_mu": self.ephemeral_information,
|
|
244
|
+
"entropy_rate": self.entropy_rate,
|
|
245
|
+
"excess_entropy": self.excess_entropy,
|
|
246
|
+
"crypticity": self.crypticity,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def block_entropy_diagram(machine: EpsilonMachine, max_length: int) -> BlockEntropyDiagram:
|
|
251
|
+
"""Compute finite-block entropy convergence curves for an epsilon-machine."""
|
|
252
|
+
if max_length < 0:
|
|
253
|
+
raise ValueError("max_length must be nonnegative")
|
|
254
|
+
|
|
255
|
+
lengths, block_entropy, state_block_entropy, block_state_entropy, pi, symbol_matrices = _block_entropy_curves(
|
|
256
|
+
machine,
|
|
257
|
+
max_length,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
entropy_rate = _entropy_rate(pi, symbol_matrices)
|
|
261
|
+
statistical_complexity = _entropy(pi)
|
|
262
|
+
excess_entropy = _excess_entropy(machine, entropy_rate=entropy_rate, block_entropy=block_entropy)
|
|
263
|
+
crypticity = statistical_complexity - excess_entropy
|
|
264
|
+
entropy_asymptote = excess_entropy + entropy_rate * lengths
|
|
265
|
+
crypticity_estimate = state_block_entropy - block_state_entropy
|
|
266
|
+
entropy_rate_estimate = np.empty(max_length + 1, dtype=float)
|
|
267
|
+
entropy_rate_estimate[0] = math.nan
|
|
268
|
+
if max_length > 0:
|
|
269
|
+
entropy_rate_estimate[1:] = np.diff(block_entropy)
|
|
270
|
+
|
|
271
|
+
return BlockEntropyDiagram(
|
|
272
|
+
lengths=lengths,
|
|
273
|
+
block_entropy=block_entropy,
|
|
274
|
+
state_block_entropy=state_block_entropy,
|
|
275
|
+
block_state_entropy=block_state_entropy,
|
|
276
|
+
entropy_asymptote=entropy_asymptote,
|
|
277
|
+
crypticity_estimate=crypticity_estimate,
|
|
278
|
+
entropy_rate_estimate=entropy_rate_estimate,
|
|
279
|
+
entropy_rate=entropy_rate,
|
|
280
|
+
excess_entropy=excess_entropy,
|
|
281
|
+
statistical_complexity=statistical_complexity,
|
|
282
|
+
crypticity=crypticity,
|
|
283
|
+
markov_order=machine.markov_order(),
|
|
284
|
+
cryptic_order=machine.cryptic_order(),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def block_entropy_estimates(
|
|
289
|
+
machine: EpsilonMachine,
|
|
290
|
+
max_length: int,
|
|
291
|
+
*,
|
|
292
|
+
entropy_rate: float | None = None,
|
|
293
|
+
use_exact: bool = True,
|
|
294
|
+
) -> BlockEntropyEstimates:
|
|
295
|
+
"""Approximate information quantities from finite block entropies.
|
|
296
|
+
|
|
297
|
+
When ``use_exact`` is true (the default) the asymptotic ``h_mu`` and excess
|
|
298
|
+
entropy come from the exact closed-form / bidirectional computation, matching
|
|
299
|
+
the diagram and block-convergence paths; when false they are promoted from the
|
|
300
|
+
finite-block curves (the last entropy-rate difference) for genuinely
|
|
301
|
+
finite-length estimates.
|
|
302
|
+
"""
|
|
303
|
+
if max_length < 0:
|
|
304
|
+
raise ValueError("max_length must be nonnegative")
|
|
305
|
+
if entropy_rate is None and max_length < 1:
|
|
306
|
+
raise ValueError("max_length must be positive when entropy_rate is not supplied")
|
|
307
|
+
|
|
308
|
+
lengths, block_entropy, state_block_entropy, block_state_entropy, pi, symbol_matrices = _block_entropy_curves(
|
|
309
|
+
machine,
|
|
310
|
+
max_length,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
entropy_rate_estimate = _entropy_rate_estimates(block_entropy)
|
|
314
|
+
if entropy_rate is not None:
|
|
315
|
+
h_mu = float(entropy_rate)
|
|
316
|
+
elif use_exact:
|
|
317
|
+
# Match the diagram / convergence paths: the closed-form entropy rate is
|
|
318
|
+
# exact for unifilar presentations and does not depend on ``max_length``.
|
|
319
|
+
h_mu = _entropy_rate(pi, symbol_matrices)
|
|
320
|
+
else:
|
|
321
|
+
h_mu = float(entropy_rate_estimate[-1])
|
|
322
|
+
statistical_complexity = _entropy(pi)
|
|
323
|
+
|
|
324
|
+
h_mu_l = h_mu * lengths
|
|
325
|
+
excess_entropy_estimate = 0.5 * (block_entropy + block_state_entropy) - h_mu_l
|
|
326
|
+
excess_entropy = _estimated_excess_entropy(machine, excess_entropy_estimate, use_exact=use_exact)
|
|
327
|
+
|
|
328
|
+
entropy_asymptote = excess_entropy + h_mu_l
|
|
329
|
+
crypticity_estimate = state_block_entropy - block_state_entropy
|
|
330
|
+
crypticity = float(crypticity_estimate[-1]) if crypticity_estimate.size else 0.0
|
|
331
|
+
|
|
332
|
+
cm = _cm_extension_curves(
|
|
333
|
+
lengths,
|
|
334
|
+
block_entropy,
|
|
335
|
+
state_block_entropy,
|
|
336
|
+
block_state_entropy,
|
|
337
|
+
h_mu=h_mu,
|
|
338
|
+
statistical_complexity=statistical_complexity,
|
|
339
|
+
excess_entropy=excess_entropy,
|
|
340
|
+
entropy_asymptote=entropy_asymptote,
|
|
341
|
+
crypticity_estimate=crypticity_estimate,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
residual_entropy = _residual_entropy_curve(machine, max_length)
|
|
345
|
+
residual_entropy_rate_estimate = _entropy_rate_estimates(residual_entropy)
|
|
346
|
+
r_mu = float(residual_entropy_rate_estimate[-1]) if max_length > 0 else math.nan
|
|
347
|
+
b_mu = h_mu - r_mu
|
|
348
|
+
rho_mu = float(block_entropy[1] - h_mu) if max_length >= 1 else math.nan
|
|
349
|
+
|
|
350
|
+
return BlockEntropyEstimates(
|
|
351
|
+
lengths=lengths,
|
|
352
|
+
block_entropy=block_entropy,
|
|
353
|
+
state_block_entropy=state_block_entropy,
|
|
354
|
+
block_state_entropy=block_state_entropy,
|
|
355
|
+
entropy_asymptote=entropy_asymptote,
|
|
356
|
+
entropy_rate_estimate=entropy_rate_estimate,
|
|
357
|
+
residual_entropy=residual_entropy,
|
|
358
|
+
residual_entropy_rate_estimate=residual_entropy_rate_estimate,
|
|
359
|
+
crypticity_estimate=crypticity_estimate,
|
|
360
|
+
entropy_rate=h_mu,
|
|
361
|
+
excess_entropy=excess_entropy,
|
|
362
|
+
statistical_complexity=statistical_complexity,
|
|
363
|
+
crypticity=crypticity,
|
|
364
|
+
predicted_information=rho_mu,
|
|
365
|
+
bound_information=b_mu,
|
|
366
|
+
ephemeral_information=r_mu,
|
|
367
|
+
excess_entropy_lower=cm.excess_entropy_lower,
|
|
368
|
+
excess_entropy_upper=cm.excess_entropy_upper,
|
|
369
|
+
excess_entropy_estimate=cm.excess_entropy_estimate,
|
|
370
|
+
synchronization_estimate=cm.synchronization_estimate,
|
|
371
|
+
reverse_synchronization_estimate=cm.reverse_synchronization_estimate,
|
|
372
|
+
transient_information_estimate=cm.transient_information_estimate,
|
|
373
|
+
predictability_gain_estimate=cm.predictability_gain_estimate,
|
|
374
|
+
oracular_information_estimate=cm.oracular_information_estimate,
|
|
375
|
+
gauge_information_estimate=cm.gauge_information_estimate,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def plot_block_entropy_diagram(
|
|
380
|
+
machine: EpsilonMachine,
|
|
381
|
+
max_length: int,
|
|
382
|
+
ax: Any | None = None,
|
|
383
|
+
**kwargs: Any,
|
|
384
|
+
) -> Any:
|
|
385
|
+
"""Compute and plot a block entropy diagram for ``machine``."""
|
|
386
|
+
return block_entropy_diagram(machine, max_length).plot(ax=ax, **kwargs)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _block_entropy_curves(
|
|
390
|
+
machine: EpsilonMachine,
|
|
391
|
+
max_length: int,
|
|
392
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, dict[Any, np.ndarray]]:
|
|
393
|
+
pi, symbol_matrices = _stationary_symbol_matrices(machine)
|
|
394
|
+
lengths = np.arange(max_length + 1, dtype=int)
|
|
395
|
+
block_entropy = np.zeros(max_length + 1, dtype=float)
|
|
396
|
+
state_block_entropy = np.zeros(max_length + 1, dtype=float)
|
|
397
|
+
block_state_entropy = np.zeros(max_length + 1, dtype=float)
|
|
398
|
+
|
|
399
|
+
word_matrices = [np.eye(len(pi), dtype=float)]
|
|
400
|
+
terminal = np.ones(len(pi), dtype=float)
|
|
401
|
+
alphabet = tuple(sorted(symbol_matrices, key=repr))
|
|
402
|
+
|
|
403
|
+
for length in range(max_length + 1):
|
|
404
|
+
block_probs: list[float] = []
|
|
405
|
+
state_block_probs: list[float] = []
|
|
406
|
+
block_state_probs: list[float] = []
|
|
407
|
+
|
|
408
|
+
for matrix in word_matrices:
|
|
409
|
+
end_mass = pi @ matrix
|
|
410
|
+
start_conditioned = matrix @ terminal
|
|
411
|
+
block_probs.append(float(end_mass.sum()))
|
|
412
|
+
state_block_probs.extend(float(prob) for prob in pi * start_conditioned)
|
|
413
|
+
block_state_probs.extend(float(prob) for prob in end_mass)
|
|
414
|
+
|
|
415
|
+
block_entropy[length] = _entropy(block_probs)
|
|
416
|
+
state_block_entropy[length] = _entropy(state_block_probs)
|
|
417
|
+
block_state_entropy[length] = _entropy(block_state_probs)
|
|
418
|
+
|
|
419
|
+
if length < max_length:
|
|
420
|
+
word_matrices = [matrix @ symbol_matrices[symbol] for matrix in word_matrices for symbol in alphabet]
|
|
421
|
+
|
|
422
|
+
return lengths, block_entropy, state_block_entropy, block_state_entropy, pi, symbol_matrices
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _stationary_symbol_matrices(machine: EpsilonMachine) -> tuple[np.ndarray, dict[Any, np.ndarray]]:
|
|
426
|
+
from sofic.generators.hmm_inference import _emission_transition_tensors
|
|
427
|
+
|
|
428
|
+
pi = machine.stationary_distribution()
|
|
429
|
+
_, raw_matrices = _emission_transition_tensors(machine)
|
|
430
|
+
n = len(pi)
|
|
431
|
+
zero = np.zeros((n, n), dtype=float)
|
|
432
|
+
matrices = {
|
|
433
|
+
symbol: np.array(raw_matrices.get(symbol, zero), dtype=float) for symbol in machine.observation_alphabet
|
|
434
|
+
}
|
|
435
|
+
return pi, matrices
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _entropy_rate(pi: np.ndarray, symbol_matrices: dict[Any, np.ndarray]) -> float:
|
|
439
|
+
rate = 0.0
|
|
440
|
+
for state_index, state_probability in enumerate(pi):
|
|
441
|
+
if state_probability <= _TOL:
|
|
442
|
+
continue
|
|
443
|
+
symbol_probs = [float(matrix[state_index].sum()) for matrix in symbol_matrices.values()]
|
|
444
|
+
rate += float(state_probability) * _entropy(symbol_probs)
|
|
445
|
+
return rate
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _excess_entropy(
|
|
449
|
+
machine: EpsilonMachine,
|
|
450
|
+
*,
|
|
451
|
+
entropy_rate: float | None = None,
|
|
452
|
+
block_entropy: np.ndarray | None = None,
|
|
453
|
+
) -> float:
|
|
454
|
+
try:
|
|
455
|
+
return float(machine.to_bidirectional().excess_entropy())
|
|
456
|
+
except Exception:
|
|
457
|
+
return _block_entropy_excess_entropy(machine, entropy_rate=entropy_rate, block_entropy=block_entropy)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _block_entropy_excess_entropy(
|
|
461
|
+
machine: EpsilonMachine,
|
|
462
|
+
*,
|
|
463
|
+
entropy_rate: float | None = None,
|
|
464
|
+
block_entropy: np.ndarray | None = None,
|
|
465
|
+
) -> float:
|
|
466
|
+
order = machine.markov_order()
|
|
467
|
+
if _is_finite_order(order):
|
|
468
|
+
markov_order = int(order)
|
|
469
|
+
h_mu = entropy_rate
|
|
470
|
+
if h_mu is None:
|
|
471
|
+
curves = _block_entropy_curves(machine, markov_order)
|
|
472
|
+
_block_entropy = curves[1]
|
|
473
|
+
pi = curves[4]
|
|
474
|
+
symbol_matrices = curves[5]
|
|
475
|
+
h_mu = _entropy_rate(pi, symbol_matrices)
|
|
476
|
+
block_entropy = _block_entropy
|
|
477
|
+
if block_entropy is not None and markov_order < len(block_entropy):
|
|
478
|
+
H_R = float(block_entropy[markov_order])
|
|
479
|
+
else:
|
|
480
|
+
curves = _block_entropy_curves(machine, markov_order)
|
|
481
|
+
H_R = float(curves[1][markov_order])
|
|
482
|
+
return float(H_R - markov_order * h_mu)
|
|
483
|
+
|
|
484
|
+
if block_entropy is None or len(block_entropy) == 0:
|
|
485
|
+
raise RuntimeError("cannot estimate excess entropy without finite block entropies")
|
|
486
|
+
h_mu = entropy_rate
|
|
487
|
+
if h_mu is None:
|
|
488
|
+
_lengths, _block_entropy, _state_block_entropy, _block_state_entropy, pi, symbol_matrices = (
|
|
489
|
+
_block_entropy_curves(machine, len(block_entropy) - 1)
|
|
490
|
+
)
|
|
491
|
+
h_mu = _entropy_rate(pi, symbol_matrices)
|
|
492
|
+
block_entropy = _block_entropy
|
|
493
|
+
length = len(block_entropy) - 1
|
|
494
|
+
return float(block_entropy[-1] - length * h_mu)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _estimated_excess_entropy(machine: EpsilonMachine, estimate: np.ndarray, *, use_exact: bool) -> float:
|
|
498
|
+
if use_exact:
|
|
499
|
+
try:
|
|
500
|
+
return float(machine.excess_entropy())
|
|
501
|
+
except Exception:
|
|
502
|
+
pass
|
|
503
|
+
return float(estimate[-1]) if estimate.size else 0.0
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _entropy_rate_estimates(entropies: np.ndarray) -> np.ndarray:
|
|
507
|
+
estimates = np.empty(len(entropies), dtype=float)
|
|
508
|
+
estimates[0] = math.nan
|
|
509
|
+
if len(entropies) > 1:
|
|
510
|
+
estimates[1:] = np.diff(entropies)
|
|
511
|
+
return estimates
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _predictability_gain(block_entropy: np.ndarray, h_mu: float) -> np.ndarray:
|
|
515
|
+
"""PG(L) = h_mu(L) - h_mu with h_mu(L) = H(L) - H(L-1) (Bialek et al., 2001).
|
|
516
|
+
|
|
517
|
+
The predictability gain is the excess of the length-``L`` entropy-rate estimate
|
|
518
|
+
over the asymptotic entropy rate, not the second difference of the block entropy.
|
|
519
|
+
"""
|
|
520
|
+
gain = np.full(len(block_entropy), math.nan, dtype=float)
|
|
521
|
+
if len(block_entropy) > 1:
|
|
522
|
+
gain[1:] = np.diff(block_entropy) - h_mu
|
|
523
|
+
return gain
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _cm_extension_curves(
|
|
527
|
+
lengths: np.ndarray,
|
|
528
|
+
block_entropy: np.ndarray,
|
|
529
|
+
state_block_entropy: np.ndarray,
|
|
530
|
+
block_state_entropy: np.ndarray,
|
|
531
|
+
*,
|
|
532
|
+
h_mu: float,
|
|
533
|
+
statistical_complexity: float,
|
|
534
|
+
excess_entropy: float,
|
|
535
|
+
entropy_asymptote: np.ndarray,
|
|
536
|
+
crypticity_estimate: np.ndarray,
|
|
537
|
+
) -> CMExtensionEstimates:
|
|
538
|
+
"""Ellison/Mahoney CM-extension curves shared by both block estimators."""
|
|
539
|
+
h_mu_l = h_mu * lengths
|
|
540
|
+
excess_entropy_lower = block_entropy - h_mu_l
|
|
541
|
+
excess_entropy_upper = block_state_entropy - h_mu_l
|
|
542
|
+
excess_entropy_estimate = 0.5 * (excess_entropy_lower + excess_entropy_upper)
|
|
543
|
+
synchronization = block_state_entropy - block_entropy
|
|
544
|
+
reverse_synchronization = state_block_entropy - block_entropy
|
|
545
|
+
transient_information = np.cumsum(entropy_asymptote - block_entropy)
|
|
546
|
+
predictability_gain = _predictability_gain(block_entropy, h_mu)
|
|
547
|
+
oracular_information = statistical_complexity + h_mu_l - state_block_entropy
|
|
548
|
+
gauge_information = statistical_complexity - excess_entropy - crypticity_estimate - oracular_information
|
|
549
|
+
return CMExtensionEstimates(
|
|
550
|
+
excess_entropy_lower=excess_entropy_lower,
|
|
551
|
+
excess_entropy_upper=excess_entropy_upper,
|
|
552
|
+
excess_entropy_estimate=excess_entropy_estimate,
|
|
553
|
+
synchronization_estimate=synchronization,
|
|
554
|
+
reverse_synchronization_estimate=reverse_synchronization,
|
|
555
|
+
transient_information_estimate=transient_information,
|
|
556
|
+
predictability_gain_estimate=predictability_gain,
|
|
557
|
+
oracular_information_estimate=oracular_information,
|
|
558
|
+
gauge_information_estimate=gauge_information,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _residual_entropy_curve(machine: EpsilonMachine, max_length: int) -> np.ndarray:
|
|
563
|
+
from sofic.generators._word_measures import residual_entropy_from_distribution
|
|
564
|
+
|
|
565
|
+
residual = np.zeros(max_length + 1, dtype=float)
|
|
566
|
+
for length in range(1, max_length + 1):
|
|
567
|
+
residual[length] = residual_entropy_from_distribution(machine.word_probabilities(length))
|
|
568
|
+
return residual
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _entropy(probabilities: Iterable[float]) -> float:
|
|
572
|
+
from sofic.generators.stochastic import shannon_entropy
|
|
573
|
+
|
|
574
|
+
return shannon_entropy(probabilities, normalize=True, atol=_TOL)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _is_finite_order(order: int | float) -> bool:
|
|
578
|
+
return not isinstance(order, float) or math.isfinite(order)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Information measures for channels presented as (epsilon-)transducers.
|
|
2
|
+
|
|
3
|
+
Following Barnett & Crutchfield (J. Stat. Phys. 161:2 (2015)), a transducer's
|
|
4
|
+
structural quantities are defined relative to a driving input process. Each
|
|
5
|
+
measure here drives the transducer with a supplied input generator, forms the
|
|
6
|
+
joint ``(input, output)`` process via
|
|
7
|
+
:func:`~sofic.automata.transducer_operations.compose_tg`, and reads off the
|
|
8
|
+
quantity -- reusing the directional-flow estimators in
|
|
9
|
+
:mod:`sofic.generators.directional_flow`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from sofic.automata.transducers import MealyMachine
|
|
21
|
+
from sofic.generators.base import HiddenMarkovModel
|
|
22
|
+
from sofic.generators.mealy import MealyHMM
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def driven_joint_generator(transducer: MealyMachine, input_process: HiddenMarkovModel) -> MealyHMM:
|
|
26
|
+
"""Return the joint ``(input, output)`` generator induced by ``input_process``."""
|
|
27
|
+
from sofic.automata.transducer_operations import compose_tg
|
|
28
|
+
|
|
29
|
+
return compose_tg(transducer, input_process, joint=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def channel_statistical_complexity(transducer: MealyMachine, input_process: HiddenMarkovModel) -> float:
|
|
33
|
+
"""Return the channel statistical complexity ``H[S]`` under ``input_process``.
|
|
34
|
+
|
|
35
|
+
``S`` is the transducer's causal-state component of the driven joint process,
|
|
36
|
+
weighted by its stationary occupation distribution.
|
|
37
|
+
"""
|
|
38
|
+
joint = driven_joint_generator(transducer, input_process)
|
|
39
|
+
idx = joint.reindex()
|
|
40
|
+
if len(idx) == 0:
|
|
41
|
+
return 0.0
|
|
42
|
+
pi = np.asarray(joint.stationary_distribution(), dtype=float)
|
|
43
|
+
mass: dict[Any, float] = defaultdict(float)
|
|
44
|
+
for state, weight in zip(idx.states, pi, strict=True):
|
|
45
|
+
transducer_state = state[1] if isinstance(state, tuple) and len(state) == 2 else state
|
|
46
|
+
mass[transducer_state] += float(weight)
|
|
47
|
+
probs = np.array([value for value in mass.values() if value > 0.0], dtype=float)
|
|
48
|
+
if probs.size == 0:
|
|
49
|
+
return 0.0
|
|
50
|
+
probs = probs / probs.sum()
|
|
51
|
+
return float(-(probs * np.log2(probs)).sum())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def driven_entropy_rate(transducer: MealyMachine, input_process: HiddenMarkovModel) -> float:
|
|
55
|
+
"""Return the entropy rate of the output process induced by ``input_process``."""
|
|
56
|
+
from sofic.automata.transducer_operations import transduce_generator
|
|
57
|
+
|
|
58
|
+
output_generator = transduce_generator(transducer, input_process)
|
|
59
|
+
return output_generator.entropy_rate()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def directed_information(transducer: MealyMachine, input_process: HiddenMarkovModel, *, length: int = 1) -> float:
|
|
63
|
+
"""Return the directed information ``I(X^n -> Y^n)`` when driven by ``input_process``."""
|
|
64
|
+
from sofic.generators.directional_flow import directed_information as _directed_information
|
|
65
|
+
|
|
66
|
+
joint = driven_joint_generator(transducer, input_process)
|
|
67
|
+
return _directed_information(joint, source="x", target="y", length=length)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def transfer_entropy(transducer: MealyMachine, input_process: HiddenMarkovModel, *, history: int = 1) -> float:
|
|
71
|
+
"""Return the input-to-output transfer entropy when driven by ``input_process``."""
|
|
72
|
+
from sofic.generators.directional_flow import transfer_entropy as _transfer_entropy
|
|
73
|
+
|
|
74
|
+
joint = driven_joint_generator(transducer, input_process)
|
|
75
|
+
return _transfer_entropy(joint, source="x", target="y", history=history)
|