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