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,1089 @@
|
|
|
1
|
+
"""Constructors for canonical ε-machines used in computational mechanics.
|
|
2
|
+
|
|
3
|
+
References
|
|
4
|
+
----------
|
|
5
|
+
- Even, ABC, SNS, NP2: Jurgens & Marzen, arXiv:1008.4182 (sync paper).
|
|
6
|
+
- Golden mean, butterfly, restricted golden mean, Nemo: Mahoney, Ellison &
|
|
7
|
+
Crutchfield, arXiv:0905.4787 (IACP); butterfly/Nemo transition tables in
|
|
8
|
+
Mahoney et al., arXiv:0906.5099.
|
|
9
|
+
- Reversible / Fig.~9 example: Ellison, Mahoney, James & Crutchfield,
|
|
10
|
+
arXiv:1107.2168.
|
|
11
|
+
- Golden-mean Markov chain (forbid ``00``): Ellison et al., arXiv:0905.3587.
|
|
12
|
+
- Golden-mean shift (forbid ``11``, Parry max-entropy): standard symbolic
|
|
13
|
+
dynamics; see e.g. Ellison et al., arXiv:1107.2168 Fig.~2.
|
|
14
|
+
- Tent map (Misiurewicz point): James, Burke & Crutchfield (2013), supplement
|
|
15
|
+
to *Chaos Forgets and Remembers*; Figs.~6--8. The ``partition`` family reads
|
|
16
|
+
the same dynamics through all four generating partitions built from the
|
|
17
|
+
critical point and its two order-1 preimages; only the kneading partition
|
|
18
|
+
appears in that paper's figures, so the three refinements are derived from the
|
|
19
|
+
interval Markov chain instead.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import math
|
|
25
|
+
from collections.abc import Hashable, Mapping, Sequence
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
31
|
+
from sofic.generators.mealy import MealyHMM
|
|
32
|
+
from sofic.graph import ATTR_EMISSION, ATTR_FUTURE_SYMBOL, ATTR_PROB, TransitionGraph
|
|
33
|
+
from sofic.shifts.tmc import TopologicalMarkovChain
|
|
34
|
+
from sofic.states import sequential_labels
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _stationary_distribution(
|
|
38
|
+
states: Sequence[Hashable],
|
|
39
|
+
symbol_matrices: Mapping[Any, np.ndarray],
|
|
40
|
+
) -> dict[Hashable, Any]:
|
|
41
|
+
from sofic.generators.prob import as_prob, has_symbolic
|
|
42
|
+
from sofic.generators.stationary import stationary_distribution_from_transition
|
|
43
|
+
|
|
44
|
+
transition = sum(symbol_matrices.values())
|
|
45
|
+
pi = stationary_distribution_from_transition(transition)
|
|
46
|
+
if pi.dtype == object or has_symbolic(pi.ravel()):
|
|
47
|
+
return {states[i]: as_prob(pi[i]) for i in range(len(states))}
|
|
48
|
+
return {states[i]: float(pi[i]) for i in range(len(states))}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def from_symbol_matrices(
|
|
52
|
+
states: Sequence[Hashable],
|
|
53
|
+
symbols: Sequence[Any],
|
|
54
|
+
matrices: Mapping[Any, np.ndarray],
|
|
55
|
+
*,
|
|
56
|
+
initial_distribution: Mapping[Hashable, Any] | None = None,
|
|
57
|
+
) -> EpsilonMachine:
|
|
58
|
+
"""Build an ε-machine from edge-labeled transition matrices ``T^(x)``.
|
|
59
|
+
|
|
60
|
+
``matrices[x][i, j]`` is ``Pr(S' = states[j], X = x | S = states[i])``.
|
|
61
|
+
Entries may be floats or exact sympy expressions.
|
|
62
|
+
"""
|
|
63
|
+
from sofic.generators.prob import as_prob, has_symbolic, is_positive_mass
|
|
64
|
+
|
|
65
|
+
state_list = tuple(states)
|
|
66
|
+
symbol_list = tuple(symbols)
|
|
67
|
+
index = {state: i for i, state in enumerate(state_list)}
|
|
68
|
+
flat_entries = [entry for matrix in matrices.values() for entry in np.asarray(matrix, dtype=object).ravel()]
|
|
69
|
+
symbolic = has_symbolic(flat_entries)
|
|
70
|
+
arrays = {}
|
|
71
|
+
for symbol, matrix in matrices.items():
|
|
72
|
+
arr = np.asarray(matrix, dtype=object if symbolic else float)
|
|
73
|
+
if arr.shape != (len(state_list), len(state_list)):
|
|
74
|
+
raise ValueError(f"matrix for symbol {symbol!r} has shape {arr.shape}")
|
|
75
|
+
arrays[symbol] = arr
|
|
76
|
+
|
|
77
|
+
pi = (
|
|
78
|
+
dict(initial_distribution) if initial_distribution is not None else _stationary_distribution(state_list, arrays)
|
|
79
|
+
)
|
|
80
|
+
eps = EpsilonMachine(
|
|
81
|
+
initial_distribution=pi,
|
|
82
|
+
observation_alphabet=frozenset(symbol_list),
|
|
83
|
+
)
|
|
84
|
+
for state in state_list:
|
|
85
|
+
eps.graph.add_state(state)
|
|
86
|
+
|
|
87
|
+
for symbol, matrix in arrays.items():
|
|
88
|
+
for source in state_list:
|
|
89
|
+
i = index[source]
|
|
90
|
+
for target in state_list:
|
|
91
|
+
j = index[target]
|
|
92
|
+
prob = as_prob(matrix[i, j])
|
|
93
|
+
if not is_positive_mass(prob):
|
|
94
|
+
continue
|
|
95
|
+
eps.graph.add_transition(
|
|
96
|
+
source,
|
|
97
|
+
target,
|
|
98
|
+
**{ATTR_PROB: prob, ATTR_EMISSION: symbol},
|
|
99
|
+
)
|
|
100
|
+
eps.validate()
|
|
101
|
+
return eps
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def bernoulli(p: float = 0.5, *, symbols: tuple[Any, Any] = ("0", "1")) -> EpsilonMachine:
|
|
105
|
+
"""Memoryless (Bernoulli) source with ``P(symbols[0]) = 1 - p``."""
|
|
106
|
+
if not 0.0 < p < 1.0:
|
|
107
|
+
raise ValueError("p must be in (0, 1)")
|
|
108
|
+
from sofic.examples.processes import _edge_machine
|
|
109
|
+
|
|
110
|
+
zero, one = symbols
|
|
111
|
+
state = sequential_labels(1)[0]
|
|
112
|
+
return _edge_machine(
|
|
113
|
+
[
|
|
114
|
+
(state, state, zero, 1.0 - p),
|
|
115
|
+
(state, state, one, p),
|
|
116
|
+
],
|
|
117
|
+
initial_distribution={state: 1.0},
|
|
118
|
+
normalize=False,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def fair_coin() -> EpsilonMachine:
|
|
123
|
+
"""Fair binary memoryless source (``p = 1/2``)."""
|
|
124
|
+
return bernoulli(0.5)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def even_process(p: float = 0.5) -> EpsilonMachine:
|
|
128
|
+
"""Even Process: even-length blocks of 1s bounded by 0s.
|
|
129
|
+
|
|
130
|
+
Jurgens & Marzen, arXiv:1008.4182, Fig.~1; Mahoney et al.,
|
|
131
|
+
arXiv:0905.4787, Sec.~III.1.
|
|
132
|
+
"""
|
|
133
|
+
if not 0.0 < p < 1.0:
|
|
134
|
+
raise ValueError("p must be in (0, 1)")
|
|
135
|
+
return from_symbol_matrices(
|
|
136
|
+
sequential_labels(2),
|
|
137
|
+
(0, 1),
|
|
138
|
+
{
|
|
139
|
+
0: np.array([[p, 0.0], [0.0, 0.0]]),
|
|
140
|
+
1: np.array([[0.0, 1.0 - p], [1.0, 0.0]]),
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def noisy_random_phase_slip() -> EpsilonMachine:
|
|
146
|
+
"""Noisy Random Phase-Slip Process (James et al., 2011, Fig.~11c).
|
|
147
|
+
|
|
148
|
+
Five-state ε-machine with stochastic phase slip at state ``A`` and
|
|
149
|
+
emission noise at state ``D``. Prototype for block-convergence figures in
|
|
150
|
+
*Anatomy of a Bit* :cite:`James2011`.
|
|
151
|
+
"""
|
|
152
|
+
from sofic.examples.processes import _edge_machine
|
|
153
|
+
|
|
154
|
+
states = sequential_labels(5)
|
|
155
|
+
a, b, c, d, e = states
|
|
156
|
+
return _edge_machine(
|
|
157
|
+
[
|
|
158
|
+
(a, a, 0, 0.5),
|
|
159
|
+
(a, b, 1, 0.5),
|
|
160
|
+
(b, c, 0, 1.0),
|
|
161
|
+
(c, d, 1, 1.0),
|
|
162
|
+
(d, e, 0, 0.5),
|
|
163
|
+
(d, e, 1, 0.5),
|
|
164
|
+
(e, a, 0, 1.0),
|
|
165
|
+
],
|
|
166
|
+
machine_type=EpsilonMachine,
|
|
167
|
+
normalize=False,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def golden_mean(p: float = 0.5) -> EpsilonMachine:
|
|
172
|
+
"""Golden Mean Process (two-state presentation, forbid consecutive ``11``).
|
|
173
|
+
|
|
174
|
+
State ``A`` self-loops on ``0`` (probability ``p``) and moves to ``B``
|
|
175
|
+
on ``1``; ``B`` always returns to ``A`` on ``0``. This is the
|
|
176
|
+
standard golden-mean *shift* topology (Mahoney et al., arXiv:0905.4787,
|
|
177
|
+
Fig.~2 style). For the order-1 Markov presentation that forbids ``00``,
|
|
178
|
+
see :func:`golden_mean_markov`; for the bidirectional machine in Ellison et
|
|
179
|
+
al., arXiv:0905.3587, Fig.~4, see :func:`golden_mean_forward` and
|
|
180
|
+
:func:`golden_mean_reverse`; for the Parry max-entropy measure on the
|
|
181
|
+
same shift, see :func:`golden_mean_shift_parry`.
|
|
182
|
+
"""
|
|
183
|
+
if not 0.0 < p < 1.0:
|
|
184
|
+
raise ValueError("p must be in (0, 1)")
|
|
185
|
+
return from_symbol_matrices(
|
|
186
|
+
sequential_labels(2),
|
|
187
|
+
(0, 1),
|
|
188
|
+
{
|
|
189
|
+
0: np.array([[p, 0.0], [1.0, 0.0]]),
|
|
190
|
+
1: np.array([[0.0, 1.0 - p], [0.0, 0.0]]),
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def golden_mean_forward(p: float = 0.5) -> EpsilonMachine:
|
|
196
|
+
"""Forward ε-machine M⁺ for the golden mean (forbid ``00``).
|
|
197
|
+
|
|
198
|
+
States ``A`` and ``B``; self-loop probability ``p = Pr(1|A)``.
|
|
199
|
+
Ellison, Mahoney & Crutchfield, arXiv:0905.3587, Fig.~4(a).
|
|
200
|
+
"""
|
|
201
|
+
if not 0.0 < p < 1.0:
|
|
202
|
+
raise ValueError("p must be in (0, 1)")
|
|
203
|
+
return from_symbol_matrices(
|
|
204
|
+
("A", "B"),
|
|
205
|
+
(0, 1),
|
|
206
|
+
{
|
|
207
|
+
0: np.array([[0.0, 1.0 - p], [0.0, 0.0]]),
|
|
208
|
+
1: np.array([[p, 0.0], [1.0, 0.0]]),
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def golden_mean_reverse(p: float = 0.5) -> EpsilonMachine:
|
|
214
|
+
"""Reverse ε-machine M⁻ for the golden mean (forbid ``00``).
|
|
215
|
+
|
|
216
|
+
Isomorphic to :func:`golden_mean_forward` with states ``A`` and ``B``.
|
|
217
|
+
When paired for a bidirectional presentation, reverse states are relabeled
|
|
218
|
+
to continue the alphabet (``C``, ``D``, ...). Ellison, Mahoney & Crutchfield,
|
|
219
|
+
arXiv:0905.3587, Fig.~4(b).
|
|
220
|
+
"""
|
|
221
|
+
return golden_mean_forward(p)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def golden_mean_bidirectional(p: float = 0.5):
|
|
225
|
+
"""Bidirectional ε-machine M± for the golden mean (forbid ``00``).
|
|
226
|
+
|
|
227
|
+
Joint states ``(A, C)``, ``(A, D)``, ``(B, C)`` as in Ellison et al.,
|
|
228
|
+
arXiv:0905.3587, Fig.~4(c).
|
|
229
|
+
"""
|
|
230
|
+
from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
|
|
231
|
+
|
|
232
|
+
return BidirectionalEpsilonMachine.from_pair(
|
|
233
|
+
golden_mean_forward(p),
|
|
234
|
+
golden_mean_reverse(p),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def golden_mean_markov(p: float = 0.5) -> EpsilonMachine:
|
|
239
|
+
"""Order-1 Markov presentation of the golden mean (forbid ``00``).
|
|
240
|
+
|
|
241
|
+
States are the most recent symbol ``0`` or ``1``. Ellison et al.,
|
|
242
|
+
arXiv:0905.3587, Fig.~1; stationary ``pi(1) = 2/3`` at ``p = 1/2``.
|
|
243
|
+
For the paper's ``A``/``B`` labeling and bidirectional machine, prefer
|
|
244
|
+
:func:`golden_mean_forward`.
|
|
245
|
+
"""
|
|
246
|
+
if not 0.0 < p < 1.0:
|
|
247
|
+
raise ValueError("p must be in (0, 1)")
|
|
248
|
+
return from_symbol_matrices(
|
|
249
|
+
sequential_labels(2),
|
|
250
|
+
(0, 1),
|
|
251
|
+
{
|
|
252
|
+
0: np.array([[0.0, 0.0], [p, 0.0]]),
|
|
253
|
+
1: np.array([[0.0, 1.0], [0.0, 1.0 - p]]),
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def golden_mean_shift_parry() -> EpsilonMachine:
|
|
259
|
+
"""Max-entropy (Parry) measure on the golden-mean shift (forbid 11).
|
|
260
|
+
|
|
261
|
+
This is the symbolic-dynamics convention (adjacency ``[[1,1],[1,0]]``),
|
|
262
|
+
distinct from :func:`golden_mean` / :func:`golden_mean_markov`, which
|
|
263
|
+
forbid consecutive 0s in the CM literature cited above.
|
|
264
|
+
"""
|
|
265
|
+
tmc = TopologicalMarkovChain.from_adjacency(
|
|
266
|
+
np.array([[1, 1], [1, 0]], dtype=float),
|
|
267
|
+
symbol_alphabet=frozenset({0, 1}),
|
|
268
|
+
)
|
|
269
|
+
parry = tmc.parry_measure()
|
|
270
|
+
return EpsilonMachine.from_hmm(parry)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def alternating_biased_coins(p: float = 0.5, q: float = 0.4) -> EpsilonMachine:
|
|
274
|
+
"""Alternating Biased Coins (ABC) Process.
|
|
275
|
+
|
|
276
|
+
Jurgens & Marzen, arXiv:1008.4182, Fig.~2.
|
|
277
|
+
"""
|
|
278
|
+
if not 0.0 < p < 1.0 or not 0.0 < q < 1.0:
|
|
279
|
+
raise ValueError("p and q must be in (0, 1)")
|
|
280
|
+
return from_symbol_matrices(
|
|
281
|
+
sequential_labels(2),
|
|
282
|
+
(0, 1),
|
|
283
|
+
{
|
|
284
|
+
0: np.array([[0.0, 1.0 - p], [1.0 - q, 0.0]]),
|
|
285
|
+
1: np.array([[0.0, p], [q, 0.0]]),
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def restricted_golden_mean(k: int = 1) -> EpsilonMachine:
|
|
291
|
+
"""Restricted Golden Mean family (``k``-cryptic); distinct from :func:`golden_mean`.
|
|
292
|
+
|
|
293
|
+
Mahoney et al., arXiv:0906.5099, Sec.~III.
|
|
294
|
+
"""
|
|
295
|
+
if k < 1:
|
|
296
|
+
raise ValueError("k must be >= 1")
|
|
297
|
+
states = sequential_labels(k + 1)
|
|
298
|
+
n = k + 1
|
|
299
|
+
t0 = np.zeros((n, n), dtype=float)
|
|
300
|
+
t1 = np.zeros((n, n), dtype=float)
|
|
301
|
+
t0[0, 1] = 0.5
|
|
302
|
+
t1[0, 0] = 0.5
|
|
303
|
+
for i in range(1, k):
|
|
304
|
+
t1[i, i + 1] = 1.0
|
|
305
|
+
t1[k, 0] = 1.0
|
|
306
|
+
return from_symbol_matrices(states, (0, 1), {0: t0, 1: t1})
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def nemo_process(p: float = 0.5, q: float = 0.5) -> EpsilonMachine:
|
|
310
|
+
"""Nemo Process (three-state, ``infty``-cryptic).
|
|
311
|
+
|
|
312
|
+
Mahoney et al., arXiv:0906.5099, Fig.~7 / Sec.~IV.
|
|
313
|
+
"""
|
|
314
|
+
if not 0.0 < p < 1.0 or not 0.0 < q < 1.0:
|
|
315
|
+
raise ValueError("p and q must be in (0, 1)")
|
|
316
|
+
states = ("A", "B", "C")
|
|
317
|
+
return from_symbol_matrices(
|
|
318
|
+
states,
|
|
319
|
+
(0, 1),
|
|
320
|
+
{
|
|
321
|
+
0: np.array(
|
|
322
|
+
[
|
|
323
|
+
[0.0, 1.0 - p, 0.0],
|
|
324
|
+
[0.0, 0.0, 1.0],
|
|
325
|
+
[1.0 - q, 0.0, 0.0],
|
|
326
|
+
]
|
|
327
|
+
),
|
|
328
|
+
1: np.array(
|
|
329
|
+
[
|
|
330
|
+
[p, 0.0, 0.0],
|
|
331
|
+
[0.0, 0.0, 0.0],
|
|
332
|
+
[q, 0.0, 0.0],
|
|
333
|
+
]
|
|
334
|
+
),
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def phase_slip_backtrack(p: float = 0.5, q: float = 0.5) -> EpsilonMachine:
|
|
340
|
+
"""Phase-Slip Backtrack (PSB) Process (``R=3``, ``k_chi=2``).
|
|
341
|
+
|
|
342
|
+
James, Mahoney, Ellison & Crutchfield, arXiv:1010.5545, Fig.~2.
|
|
343
|
+
"""
|
|
344
|
+
if not 0.0 < p < 1.0 or not 0.0 < q < 1.0:
|
|
345
|
+
raise ValueError("p and q must be in (0, 1)")
|
|
346
|
+
states = ("A", "B", "C", "D")
|
|
347
|
+
return from_symbol_matrices(
|
|
348
|
+
states,
|
|
349
|
+
(0, 1),
|
|
350
|
+
{
|
|
351
|
+
0: np.array(
|
|
352
|
+
[
|
|
353
|
+
[0.0, 0.0, 1.0 - p, 0.0],
|
|
354
|
+
[0.0, 0.0, 0.0, 1.0 - q],
|
|
355
|
+
[1.0, 0.0, 0.0, 0.0],
|
|
356
|
+
[0.0, 0.0, 0.0, 0.0],
|
|
357
|
+
]
|
|
358
|
+
),
|
|
359
|
+
1: np.array(
|
|
360
|
+
[
|
|
361
|
+
[0.0, p, 0.0, 0.0],
|
|
362
|
+
[0.0, 0.0, q, 0.0],
|
|
363
|
+
[0.0, 0.0, 0.0, 0.0],
|
|
364
|
+
[0.0, 1.0, 0.0, 0.0],
|
|
365
|
+
]
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def butterfly_process() -> EpsilonMachine:
|
|
372
|
+
"""Butterfly Process (five-state, ``2``-cryptic) over symbols ``0``--``7``.
|
|
373
|
+
|
|
374
|
+
Mahoney et al., arXiv:0906.5099, Fig.~1. Each causal state emits every
|
|
375
|
+
symbol with probability ``1/8``; synchronizing symbols ``2``--``7`` always
|
|
376
|
+
reach the same causal state regardless of the source.
|
|
377
|
+
"""
|
|
378
|
+
states = ("A", "B", "C", "D", "E")
|
|
379
|
+
prob = 1.0 / 8.0
|
|
380
|
+
targets = {
|
|
381
|
+
0: {"A": "B", "B": "B", "C": "D", "D": "B", "E": "D"},
|
|
382
|
+
1: {"A": "C", "B": "C", "C": "C", "D": "E", "E": "E"},
|
|
383
|
+
2: "A",
|
|
384
|
+
3: "A",
|
|
385
|
+
4: "B",
|
|
386
|
+
5: "D",
|
|
387
|
+
6: "C",
|
|
388
|
+
7: "E",
|
|
389
|
+
}
|
|
390
|
+
from sofic.examples.processes import _edge_machine
|
|
391
|
+
|
|
392
|
+
edges = []
|
|
393
|
+
for source in states:
|
|
394
|
+
for symbol in range(8):
|
|
395
|
+
target_spec = targets[symbol]
|
|
396
|
+
target = target_spec if isinstance(target_spec, str) else target_spec[source]
|
|
397
|
+
edges.append((source, target, symbol, prob))
|
|
398
|
+
return _edge_machine(
|
|
399
|
+
edges,
|
|
400
|
+
initial_distribution=dict.fromkeys(states, 0.2),
|
|
401
|
+
normalize=False,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def ellison_fig9_forward() -> EpsilonMachine:
|
|
406
|
+
"""Forward ε-machine from Ellison et al., arXiv:1107.2168, Fig.~9."""
|
|
407
|
+
from sofic.examples.processes import _edge_machine
|
|
408
|
+
|
|
409
|
+
return _edge_machine(
|
|
410
|
+
[
|
|
411
|
+
("A", "A", 0, 0.5),
|
|
412
|
+
("A", "B", 1, 0.5),
|
|
413
|
+
("B", "B", 1, 0.5),
|
|
414
|
+
("B", "A", 2, 0.5),
|
|
415
|
+
],
|
|
416
|
+
initial_distribution={"A": 0.5, "B": 0.5},
|
|
417
|
+
normalize=False,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def tent_map_misiurewicz_a(symbolic: bool = False):
|
|
422
|
+
"""Misiurewicz parameter ``a`` for the tent map (James et al., 2013, Eq. 12).
|
|
423
|
+
|
|
424
|
+
With ``symbolic=True``, return the exact sympy expression
|
|
425
|
+
``α + 2/(3α)`` where ``α = (1 + sqrt(19/27))**(1/3)``.
|
|
426
|
+
"""
|
|
427
|
+
if symbolic:
|
|
428
|
+
import sympy as sp
|
|
429
|
+
|
|
430
|
+
alpha = (1 + sp.sqrt(sp.Rational(19, 27))) ** sp.Rational(1, 3)
|
|
431
|
+
return sp.simplify(alpha + 2 / (3 * alpha))
|
|
432
|
+
alpha = (math.sqrt(19 / 27) + 1) ** (1 / 3)
|
|
433
|
+
return alpha + 2 / (3 * alpha)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def tent_map_misiurewicz_fig7_symbol_matrices(
|
|
437
|
+
a: Any | None = None,
|
|
438
|
+
) -> tuple[tuple[str, ...], tuple[int, ...], dict[int, np.ndarray]]:
|
|
439
|
+
"""Return Fig.~7 ε-machine symbol matrices for the tent map at parameter ``a``.
|
|
440
|
+
|
|
441
|
+
James, Burke & Crutchfield, *Chaos Forgets and Remembers* (2013), supplement
|
|
442
|
+
Fig.~7. State ``A`` emits only ``1``; ``D`` has a nontrivial ``0`` branch to
|
|
443
|
+
``C`` and a ``1`` self-loop. When ``a`` is a sympy expression the matrices
|
|
444
|
+
use object dtype with exact entries.
|
|
445
|
+
|
|
446
|
+
This is the ``"c"`` member of the four-partition family of
|
|
447
|
+
:func:`tent_map_misiurewicz_partition_symbol_matrices`, keeping the figure's
|
|
448
|
+
state names. That function instead names states by decreasing stationary
|
|
449
|
+
probability, so its ``A, B, C, D`` are this function's ``D, C, B, A``.
|
|
450
|
+
"""
|
|
451
|
+
from sofic.generators.prob import is_symbolic, zeros
|
|
452
|
+
|
|
453
|
+
if a is None:
|
|
454
|
+
a = tent_map_misiurewicz_a()
|
|
455
|
+
states = ("A", "B", "C", "D")
|
|
456
|
+
symbolic = is_symbolic(a)
|
|
457
|
+
denom = 2 * a**2 + 4 * a + 2
|
|
458
|
+
t0 = zeros((4, 4), symbolic=symbolic)
|
|
459
|
+
t1 = zeros((4, 4), symbolic=symbolic)
|
|
460
|
+
# Topology from supplement Fig. 7.
|
|
461
|
+
t1[0, 1] = 1 if symbolic else 1.0 # A → B on 1
|
|
462
|
+
t0[1, 2] = (a + 2) / (2 * a + 2) # B → C on 0
|
|
463
|
+
t1[1, 0] = a / (2 * a + 2) # B → A on 1
|
|
464
|
+
t0[2, 0] = 1 / (a + 2) if symbolic else 1.0 / (a + 2) # C → A on 0
|
|
465
|
+
t1[2, 3] = (a + 1) / (a + 2) # C → D on 1
|
|
466
|
+
t0[3, 2] = (a**2 + 2 * a) / denom # D → C on 0
|
|
467
|
+
t1[3, 3] = (a**2 + 2 * a + 2) / denom # D → D on 1
|
|
468
|
+
return states, (0, 1), {0: t0, 1: t1}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def tent_map_misiurewicz_forward(a: Any | None = None) -> EpsilonMachine:
|
|
472
|
+
"""Forward ε-machine for tent-map symbolic dynamics at the Misiurewicz point."""
|
|
473
|
+
states, symbols, matrices = tent_map_misiurewicz_fig7_symbol_matrices(a)
|
|
474
|
+
return from_symbol_matrices(states, symbols, matrices)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
#: Keys of the four generating partitions of the tent map at the Misiurewicz
|
|
478
|
+
#: point, in refinement order. Each names the cuts added to the critical point
|
|
479
|
+
#: ``c = 1/2``: nothing, the left preimage ``L = 1/(2a)``, the right preimage
|
|
480
|
+
#: ``R = 1 - 1/(2a)``, or both. See
|
|
481
|
+
#: :func:`tent_map_misiurewicz_partition_symbol_matrices`.
|
|
482
|
+
TENT_MAP_MISIUREWICZ_PARTITIONS: tuple[str, ...] = ("c", "Lc", "cR", "LcR")
|
|
483
|
+
|
|
484
|
+
#: ``partition -> (states, alphabet, edges)`` where each edge is
|
|
485
|
+
#: ``(source, symbol, target, (k0, k1, k2), d)`` standing for the transition
|
|
486
|
+
#: probability ``(k0 + k1 * a + k2 * a**2) / d``. Every entry is reduced modulo
|
|
487
|
+
#: the parameter's minimal polynomial ``a**3 = 2a + 2``, which is why no
|
|
488
|
+
#: probability carries an ``a``-dependent denominator. States are named by
|
|
489
|
+
#: decreasing stationary probability, uniformly across the four partitions.
|
|
490
|
+
_TENT_MAP_PARTITION_EDGES: dict[
|
|
491
|
+
str,
|
|
492
|
+
tuple[tuple[str, ...], tuple[int, ...], tuple[tuple[str, int, str, tuple[int, int, int], int], ...]],
|
|
493
|
+
] = {
|
|
494
|
+
"c": (
|
|
495
|
+
("A", "B", "C", "D"),
|
|
496
|
+
(0, 1),
|
|
497
|
+
(
|
|
498
|
+
("A", 0, "B", (4, 0, -1), 2),
|
|
499
|
+
("A", 1, "A", (-2, 0, 1), 2),
|
|
500
|
+
("B", 0, "D", (2, -2, 1), 6),
|
|
501
|
+
("B", 1, "A", (4, 2, -1), 6),
|
|
502
|
+
("C", 0, "B", (0, -1, 1), 2),
|
|
503
|
+
("C", 1, "D", (2, 1, -1), 2),
|
|
504
|
+
("D", 1, "C", (1, 0, 0), 1),
|
|
505
|
+
),
|
|
506
|
+
),
|
|
507
|
+
"Lc": (
|
|
508
|
+
("A", "B", "C", "D", "E"),
|
|
509
|
+
(0, 1, 2),
|
|
510
|
+
(
|
|
511
|
+
("A", 0, "E", (2, -1, 0), 2),
|
|
512
|
+
("A", 1, "B", (2, 1, -1), 2),
|
|
513
|
+
("A", 2, "A", (-2, 0, 1), 2),
|
|
514
|
+
("B", 2, "A", (1, 0, 0), 1),
|
|
515
|
+
("C", 0, "E", (-1, -1, 1), 2),
|
|
516
|
+
("C", 1, "B", (1, 0, 0), 2),
|
|
517
|
+
("C", 2, "D", (2, 1, -1), 2),
|
|
518
|
+
("D", 2, "C", (1, 0, 0), 1),
|
|
519
|
+
("E", 1, "D", (1, 0, 0), 1),
|
|
520
|
+
),
|
|
521
|
+
),
|
|
522
|
+
"cR": (
|
|
523
|
+
("A", "B", "C", "D", "E"),
|
|
524
|
+
(0, 1, 2),
|
|
525
|
+
(
|
|
526
|
+
("A", 0, "B", (1, 0, 0), 1),
|
|
527
|
+
("B", 0, "D", (2, -2, 1), 6),
|
|
528
|
+
("B", 1, "C", (-2, -1, 2), 6),
|
|
529
|
+
("B", 2, "A", (2, 1, -1), 2),
|
|
530
|
+
("C", 1, "C", (-2, 0, 1), 2),
|
|
531
|
+
("C", 2, "A", (4, 0, -1), 2),
|
|
532
|
+
("D", 1, "E", (2, 1, -1), 2),
|
|
533
|
+
("D", 2, "A", (0, -1, 1), 2),
|
|
534
|
+
("E", 1, "D", (1, 0, 0), 1),
|
|
535
|
+
),
|
|
536
|
+
),
|
|
537
|
+
"LcR": (
|
|
538
|
+
("A", "B", "C", "D", "E"),
|
|
539
|
+
(0, 1, 2, 3),
|
|
540
|
+
(
|
|
541
|
+
("A", 2, "A", (-2, 0, 1), 2),
|
|
542
|
+
("A", 3, "B", (4, 0, -1), 2),
|
|
543
|
+
("B", 0, "D", (2, -2, 1), 6),
|
|
544
|
+
("B", 1, "A", (4, 2, -1), 6),
|
|
545
|
+
("C", 2, "E", (2, 1, -1), 2),
|
|
546
|
+
("C", 3, "B", (0, -1, 1), 2),
|
|
547
|
+
("D", 1, "C", (1, 0, 0), 1),
|
|
548
|
+
("E", 2, "C", (1, 0, 0), 1),
|
|
549
|
+
),
|
|
550
|
+
),
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#: ``partition -> ((k0, k1, k2), d)`` for the ephemeral information rate
|
|
554
|
+
#: ``r_mu = (k0 + k1 * a + k2 * a**2) / d``, again reduced modulo
|
|
555
|
+
#: ``a**3 = 2a + 2``. For ``"c"`` this is the published rate of James et al.
|
|
556
|
+
#: (2013) in reduced form; see
|
|
557
|
+
#: :func:`tent_map_misiurewicz_partition_information_expected`.
|
|
558
|
+
_TENT_MAP_PARTITION_EPHEMERAL: dict[str, tuple[tuple[int, int, int], int]] = {
|
|
559
|
+
"c": ((59, 7, -11), 57),
|
|
560
|
+
"Lc": ((56, 25, -23), 57),
|
|
561
|
+
"cR": ((1, -6, 4), 19),
|
|
562
|
+
"LcR": ((0, 0, 0), 1),
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _tent_map_partition_check(partition: str) -> None:
|
|
567
|
+
if partition not in _TENT_MAP_PARTITION_EDGES:
|
|
568
|
+
raise ValueError(f"unknown tent-map partition {partition!r}; expected one of {TENT_MAP_MISIUREWICZ_PARTITIONS}")
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _tent_map_quadratic(coeffs: tuple[int, int, int], denom: int, a: Any, symbolic: bool) -> Any:
|
|
572
|
+
"""Evaluate ``(k0 + k1 * a + k2 * a**2) / denom`` exactly or in floats."""
|
|
573
|
+
k0, k1, k2 = coeffs
|
|
574
|
+
if symbolic:
|
|
575
|
+
import sympy as sp
|
|
576
|
+
|
|
577
|
+
return sp.Rational(k0, denom) + sp.Rational(k1, denom) * a + sp.Rational(k2, denom) * a**2
|
|
578
|
+
return (k0 + k1 * a + k2 * a**2) / denom
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def tent_map_misiurewicz_partition_cuts(partition: str, a: Any | None = None) -> tuple[Any, ...]:
|
|
582
|
+
"""Return the ascending cut points of one of the four tent-map partitions.
|
|
583
|
+
|
|
584
|
+
``"c"`` cuts only at the critical point; ``"Lc"`` and ``"cR"`` add one
|
|
585
|
+
order-1 preimage of it; ``"LcR"`` adds both. With a symbolic ``a`` the cuts
|
|
586
|
+
are exact sympy expressions.
|
|
587
|
+
"""
|
|
588
|
+
_tent_map_partition_check(partition)
|
|
589
|
+
from sofic.generators.prob import is_symbolic
|
|
590
|
+
|
|
591
|
+
if a is None:
|
|
592
|
+
a = tent_map_misiurewicz_a()
|
|
593
|
+
if is_symbolic(a):
|
|
594
|
+
import sympy as sp
|
|
595
|
+
|
|
596
|
+
c = sp.Rational(1, 2)
|
|
597
|
+
left = 1 / (2 * a)
|
|
598
|
+
else:
|
|
599
|
+
c = 0.5
|
|
600
|
+
left = 1.0 / (2.0 * a)
|
|
601
|
+
right = 1 - left
|
|
602
|
+
return {"c": (c,), "Lc": (left, c), "cR": (c, right), "LcR": (left, c, right)}[partition]
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def tent_map_misiurewicz_partition_symbol_matrices(
|
|
606
|
+
partition: str,
|
|
607
|
+
a: Any | None = None,
|
|
608
|
+
) -> tuple[tuple[str, ...], tuple[int, ...], dict[int, np.ndarray]]:
|
|
609
|
+
"""Symbol matrices ``T^(x)`` for one of the tent map's four generating partitions.
|
|
610
|
+
|
|
611
|
+
At the Misiurewicz parameter the interval ``[0, 1]`` can be cut at the
|
|
612
|
+
critical point ``c = 1/2`` and, optionally, at either or both of its
|
|
613
|
+
order-1 preimages ``L = 1/(2a)`` and ``R = 1 - 1/(2a)``. All four choices
|
|
614
|
+
are generating, so all four read out the *same* dynamics at the same entropy
|
|
615
|
+
rate ``h_mu = log2(a)``; they differ in how many letters they spend and in
|
|
616
|
+
how much of that rate survives as bound information:
|
|
617
|
+
|
|
618
|
+
========== ========================= ====== ======== ================
|
|
619
|
+
Partition Cells States Alphabet ``r_mu``
|
|
620
|
+
========== ========================= ====== ======== ================
|
|
621
|
+
``"c"`` ``c`` 4 2 ``(59 + 7a - 11a**2)/57``
|
|
622
|
+
``"Lc"`` ``L, c`` 5 3 ``(56 + 25a - 23a**2)/57``
|
|
623
|
+
``"cR"`` ``c, R`` 5 3 ``(1 - 6a + 4a**2)/19``
|
|
624
|
+
``"LcR"`` ``L, c, R`` 5 4 ``0``
|
|
625
|
+
========== ========================= ====== ======== ================
|
|
626
|
+
|
|
627
|
+
Symbols number the cells left to right, so ``"LcR"`` emits ``0`` on
|
|
628
|
+
``[0, L)``, ``1`` on ``[L, c)``, ``2`` on ``[c, R)`` and ``3`` on
|
|
629
|
+
``[R, 1]``. Reducing by the parameter's minimal polynomial
|
|
630
|
+
``a**3 = 2a + 2`` makes every transition probability a quadratic in ``a``
|
|
631
|
+
with rational coefficients, so none of them carries an ``a``-dependent
|
|
632
|
+
denominator. States are named by decreasing stationary probability in every
|
|
633
|
+
partition, which makes them comparable across the family; for ``"c"`` that
|
|
634
|
+
relabels the published figure, whose ``A, B, C, D`` are this function's
|
|
635
|
+
``D, C, B, A`` (see :func:`tent_map_misiurewicz_fig7_symbol_matrices`).
|
|
636
|
+
|
|
637
|
+
Derived from the exact interval Markov chain on the forward-orbit closure of
|
|
638
|
+
``{c, L, R}``. The tent map, the Misiurewicz parameter and the ``"c"``
|
|
639
|
+
presentation are from James, Burke & Crutchfield, *Chaos Forgets and
|
|
640
|
+
Remembers* (2013) :cite:`James2013`; that paper's figures cover only the
|
|
641
|
+
kneading partition, so the three refinements have no published figure to
|
|
642
|
+
cite.
|
|
643
|
+
"""
|
|
644
|
+
_tent_map_partition_check(partition)
|
|
645
|
+
from sofic.generators.prob import is_symbolic, zeros
|
|
646
|
+
|
|
647
|
+
if a is None:
|
|
648
|
+
a = tent_map_misiurewicz_a()
|
|
649
|
+
states, alphabet, edges = _TENT_MAP_PARTITION_EDGES[partition]
|
|
650
|
+
symbolic = is_symbolic(a)
|
|
651
|
+
index = {state: i for i, state in enumerate(states)}
|
|
652
|
+
size = len(states)
|
|
653
|
+
matrices = {symbol: zeros((size, size), symbolic=symbolic) for symbol in alphabet}
|
|
654
|
+
for source, symbol, target, coeffs, denom in edges:
|
|
655
|
+
matrices[symbol][index[source], index[target]] = _tent_map_quadratic(coeffs, denom, a, symbolic)
|
|
656
|
+
return states, alphabet, matrices
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def tent_map_misiurewicz_partition_forward(partition: str, a: Any | None = None) -> EpsilonMachine:
|
|
660
|
+
"""Forward ε-machine of the tent map under one of its four generating partitions.
|
|
661
|
+
|
|
662
|
+
See :func:`tent_map_misiurewicz_partition_symbol_matrices` for the partitions
|
|
663
|
+
and their presentations. Every one of the four is unifilar and strictly
|
|
664
|
+
sofic -- Markov and cryptic orders are infinite throughout -- so refining the
|
|
665
|
+
partition never buys finite memory. What it buys is bound information:
|
|
666
|
+
``r_mu`` falls from ``0.6483`` bits/symbol at ``"c"`` to exactly zero at
|
|
667
|
+
``"LcR"``.
|
|
668
|
+
|
|
669
|
+
``"c"`` is :func:`tent_map_misiurewicz_forward` up to the state relabeling
|
|
670
|
+
noted in :func:`tent_map_misiurewicz_partition_symbol_matrices`.
|
|
671
|
+
"""
|
|
672
|
+
states, symbols, matrices = tent_map_misiurewicz_partition_symbol_matrices(partition, a)
|
|
673
|
+
return from_symbol_matrices(states, symbols, matrices)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def tent_map_misiurewicz_partition_information_expected(
|
|
677
|
+
partition: str,
|
|
678
|
+
a: Any | None = None,
|
|
679
|
+
) -> dict[str, Any]:
|
|
680
|
+
"""Closed-form anatomy of one of the tent map's four generating partitions.
|
|
681
|
+
|
|
682
|
+
All four are generating, so ``entropy_rate = log2(a)`` throughout and only
|
|
683
|
+
the split into ``bound_mu`` and ``ephemeral_mu`` changes. Each ephemeral
|
|
684
|
+
rate is a quadratic in ``a`` with rational coefficients, tabulated in
|
|
685
|
+
:func:`tent_map_misiurewicz_partition_symbol_matrices`.
|
|
686
|
+
|
|
687
|
+
Two exact facts about the family are worth noting. First, ``"LcR"`` has
|
|
688
|
+
``r_mu = 0``: its machine is unifilar, no two edges share both a source and
|
|
689
|
+
a target, and every branch leads to a state with a distinguishable future,
|
|
690
|
+
so the past fixes the causal state, the future fixes the successor, and the
|
|
691
|
+
two together name the emitted symbol -- leaving nothing for
|
|
692
|
+
``r_mu = H[X_0 | past, future]`` to measure. Second, the ephemeral rate is
|
|
693
|
+
*modular* over the two available cuts,
|
|
694
|
+
|
|
695
|
+
``r_mu("c") - r_mu("Lc") - r_mu("cR") + r_mu("LcR") = 0``
|
|
696
|
+
|
|
697
|
+
identically in ``a``, so each cut is worth a fixed number of bits regardless
|
|
698
|
+
of whether the other has been made; the ``L`` cut is worth
|
|
699
|
+
``r_mu("cR") = (1 - 6a + 4a**2)/19``, which is also exactly the invariant
|
|
700
|
+
measure of the two fine cells it separates.
|
|
701
|
+
|
|
702
|
+
For ``"c"`` this reproduces :func:`tent_map_misiurewicz_information_expected`,
|
|
703
|
+
which states the same rate in the unreduced form published by James, Burke &
|
|
704
|
+
Crutchfield (2013) :cite:`James2013`.
|
|
705
|
+
"""
|
|
706
|
+
_tent_map_partition_check(partition)
|
|
707
|
+
from sofic.generators.prob import is_symbolic
|
|
708
|
+
|
|
709
|
+
if a is None:
|
|
710
|
+
a = tent_map_misiurewicz_a()
|
|
711
|
+
coeffs, denom = _TENT_MAP_PARTITION_EPHEMERAL[partition]
|
|
712
|
+
if is_symbolic(a):
|
|
713
|
+
import sympy as sp
|
|
714
|
+
|
|
715
|
+
h_mu = sp.log(a, 2)
|
|
716
|
+
r_mu = sp.simplify(_tent_map_quadratic(coeffs, denom, a, True))
|
|
717
|
+
return {"bound_mu": sp.simplify(h_mu - r_mu), "ephemeral_mu": r_mu, "entropy_rate": h_mu}
|
|
718
|
+
h_mu = math.log2(a)
|
|
719
|
+
r_mu = _tent_map_quadratic(coeffs, denom, a, False)
|
|
720
|
+
return {"bound_mu": h_mu - r_mu, "ephemeral_mu": r_mu, "entropy_rate": h_mu}
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def tent_map_misiurewicz_hmm(a: Any | None = None) -> MealyHMM:
|
|
724
|
+
"""Non-unifilar HMM from supplement Fig.~6 (right).
|
|
725
|
+
|
|
726
|
+
James, Burke & Crutchfield, *Chaos Forgets and Remembers* (2013), supplement
|
|
727
|
+
Fig.~6 (right): generating partition overlaid on the Markov-partition chain.
|
|
728
|
+
Non-unifilar at ``A`` (two ``0`` outs) and ``D`` (three ``1`` outs).
|
|
729
|
+
:meth:`~sofic.generators.epsilon_machine.EpsilonMachine.from_hmm` recovers
|
|
730
|
+
the Fig.~7 ε-machine.
|
|
731
|
+
"""
|
|
732
|
+
from sofic.generators.prob import as_prob, is_symbolic
|
|
733
|
+
|
|
734
|
+
if a is None:
|
|
735
|
+
a = tent_map_misiurewicz_a()
|
|
736
|
+
symbolic = is_symbolic(a)
|
|
737
|
+
if symbolic:
|
|
738
|
+
import sympy as sp
|
|
739
|
+
|
|
740
|
+
half = sp.Rational(1, 2)
|
|
741
|
+
one = sp.Integer(1)
|
|
742
|
+
inv_a1 = 1 / (a + 1)
|
|
743
|
+
half_a_a1 = a / (2 * (a + 1))
|
|
744
|
+
else:
|
|
745
|
+
half = 0.5
|
|
746
|
+
one = 1.0
|
|
747
|
+
inv_a1 = 1.0 / (a + 1.0)
|
|
748
|
+
half_a_a1 = a / (2.0 * (a + 1.0))
|
|
749
|
+
|
|
750
|
+
hmm = MealyHMM(observation_alphabet=frozenset({0, 1}))
|
|
751
|
+
for state in ("A", "B", "C", "D"):
|
|
752
|
+
hmm.graph.add_state(state)
|
|
753
|
+
edges = [
|
|
754
|
+
("A", "B", 0, half),
|
|
755
|
+
("A", "C", 0, half),
|
|
756
|
+
("B", "D", 0, one),
|
|
757
|
+
("C", "D", 1, one),
|
|
758
|
+
("D", "A", 1, inv_a1),
|
|
759
|
+
("D", "B", 1, half_a_a1),
|
|
760
|
+
("D", "C", 1, half_a_a1),
|
|
761
|
+
]
|
|
762
|
+
for source, target, symbol, prob in edges:
|
|
763
|
+
hmm.add_transition(source, target, symbol, as_prob(prob))
|
|
764
|
+
|
|
765
|
+
if symbolic:
|
|
766
|
+
# The Misiurewicz parameter is the real root of ``a**3 - 2*a - 2`` (James,
|
|
767
|
+
# Burke & Crutchfield, 2013, Eq. 12; the Cardano form ``alpha + 2/(3 alpha)``
|
|
768
|
+
# returned by ``tent_map_misiurewicz_a(symbolic=True)``). Carrying this
|
|
769
|
+
# minimal polynomial lets ``EpsilonMachine.from_hmm`` recognize the two
|
|
770
|
+
# mixed states that coincide only under the constraint and recover the
|
|
771
|
+
# 4-state Fig.~7 machine.
|
|
772
|
+
from sofic.generators.prob import SymbolConstraints
|
|
773
|
+
|
|
774
|
+
hmm.symbol_constraints = SymbolConstraints([a**3 - 2 * a - 2])
|
|
775
|
+
|
|
776
|
+
pi = hmm.stationary_distribution()
|
|
777
|
+
idx = hmm.reindex()
|
|
778
|
+
if pi.dtype == object or symbolic:
|
|
779
|
+
hmm.initial_distribution = {idx.state(i): as_prob(pi[i]) for i in range(len(idx.states))}
|
|
780
|
+
else:
|
|
781
|
+
hmm.initial_distribution = {idx.state(i): float(pi[i]) for i in range(len(idx.states))}
|
|
782
|
+
return hmm
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def tent_map_misiurewicz_reverse(a: float | None = None) -> EpsilonMachine:
|
|
786
|
+
"""Reverse ε-machine for tent-map symbolic dynamics at the Misiurewicz point.
|
|
787
|
+
|
|
788
|
+
Projected from the supplement Fig.~8 bidirectional presentation with
|
|
789
|
+
``future_symbol`` annotations on ``E``, ``F``, and ``G``.
|
|
790
|
+
"""
|
|
791
|
+
return tent_map_misiurewicz_bidirectional_fig8(a).reverse_machine
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _annotate_tent_map_misiurewicz_reverse_future_symbols(reverse: EpsilonMachine) -> None:
|
|
795
|
+
"""Attach synchronization symbols to reverse causal states (supplement Fig.~8)."""
|
|
796
|
+
# Present-symbol constraints on reverse components E,F,G; H is outside support.
|
|
797
|
+
futures = {"E": 0, "F": 1, "G": 1, "H": -1}
|
|
798
|
+
for state, symbol in futures.items():
|
|
799
|
+
if not reverse.graph.has_state(state):
|
|
800
|
+
continue
|
|
801
|
+
attrs = dict(reverse.graph.state_attrs(state))
|
|
802
|
+
attrs[ATTR_FUTURE_SYMBOL] = symbol
|
|
803
|
+
reverse.graph.add_state(state, **attrs)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _tent_map_misiurewicz_fig8_reverse_relabel() -> dict[str, str]:
|
|
807
|
+
"""Map supplement reverse causal labels to collision-free joint labels."""
|
|
808
|
+
return {"A": "E", "B": "F", "C": "G", "D": "H"}
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _tent_map_misiurewicz_fig8_joint_state(
|
|
812
|
+
forward: str,
|
|
813
|
+
reverse: str,
|
|
814
|
+
*,
|
|
815
|
+
relabel: Mapping[str, str],
|
|
816
|
+
) -> tuple[str, str]:
|
|
817
|
+
return forward, relabel[reverse]
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _tent_map_misiurewicz_fig8_edges(
|
|
821
|
+
a: Any,
|
|
822
|
+
) -> list[tuple[tuple[str, str], tuple[str, str], int, Any]]:
|
|
823
|
+
"""Directed edges for supplement Fig.~8 with reverse states relabeled E--H.
|
|
824
|
+
|
|
825
|
+
Joint labels use ``S⁺:S⁻`` from James et al. (2013), supplement Fig.~8.
|
|
826
|
+
Edge probabilities are the figure's ``1/2`` and ``a/(a+1)`` templates.
|
|
827
|
+
"""
|
|
828
|
+
from sofic.generators.prob import is_symbolic
|
|
829
|
+
|
|
830
|
+
relabel = _tent_map_misiurewicz_fig8_reverse_relabel()
|
|
831
|
+
name = {
|
|
832
|
+
"BA": ("B", "A"),
|
|
833
|
+
"CC": ("C", "C"),
|
|
834
|
+
"AB": ("A", "B"),
|
|
835
|
+
"BC": ("B", "C"),
|
|
836
|
+
"CB": ("C", "B"),
|
|
837
|
+
"DA": ("D", "A"),
|
|
838
|
+
"DB": ("D", "B"),
|
|
839
|
+
"DC": ("D", "C"),
|
|
840
|
+
}
|
|
841
|
+
if is_symbolic(a):
|
|
842
|
+
import sympy as sp
|
|
843
|
+
|
|
844
|
+
half = sp.Rational(1, 2)
|
|
845
|
+
inv_a1 = 1 / (a + 1)
|
|
846
|
+
frac_a1 = a / (a + 1)
|
|
847
|
+
one = sp.Integer(1)
|
|
848
|
+
else:
|
|
849
|
+
half = 0.5
|
|
850
|
+
inv_a1 = 1.0 / (a + 1.0)
|
|
851
|
+
frac_a1 = a / (a + 1.0)
|
|
852
|
+
one = 1.0
|
|
853
|
+
edge_specs = [
|
|
854
|
+
("BA", "CC", 0, one),
|
|
855
|
+
("CC", "AB", 0, half),
|
|
856
|
+
("CC", "DB", 1, half),
|
|
857
|
+
("AB", "BA", 1, inv_a1),
|
|
858
|
+
("AB", "BC", 1, frac_a1),
|
|
859
|
+
("BC", "AB", 1, half),
|
|
860
|
+
("BC", "CB", 0, half),
|
|
861
|
+
("DA", "CC", 0, one),
|
|
862
|
+
("DB", "DA", 1, inv_a1),
|
|
863
|
+
("DB", "DC", 1, frac_a1),
|
|
864
|
+
("DC", "DB", 1, half),
|
|
865
|
+
("DC", "CB", 0, half),
|
|
866
|
+
("CB", "DC", 1, frac_a1),
|
|
867
|
+
("CB", "DA", 1, inv_a1),
|
|
868
|
+
]
|
|
869
|
+
edges: list[tuple[tuple[str, str], tuple[str, str], int, Any]] = []
|
|
870
|
+
for row, target, symbol, prob in edge_specs:
|
|
871
|
+
source = _tent_map_misiurewicz_fig8_joint_state(name[row][0], name[row][1], relabel=relabel)
|
|
872
|
+
dest = _tent_map_misiurewicz_fig8_joint_state(name[target][0], name[target][1], relabel=relabel)
|
|
873
|
+
edges.append((source, dest, symbol, prob))
|
|
874
|
+
return edges
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _stationary_distribution_from_joint_graph(
|
|
878
|
+
graph: TransitionGraph,
|
|
879
|
+
) -> dict[tuple[str, str], Any]:
|
|
880
|
+
from sofic.generators.prob import as_prob, has_symbolic, zeros
|
|
881
|
+
from sofic.generators.stationary import stationary_distribution_from_transition
|
|
882
|
+
|
|
883
|
+
states = list(graph.states())
|
|
884
|
+
if not states:
|
|
885
|
+
return {}
|
|
886
|
+
index = {state: position for position, state in enumerate(states)}
|
|
887
|
+
edge_probs = [t.data.get(ATTR_PROB, 0.0) for t in graph.transitions()]
|
|
888
|
+
symbolic = has_symbolic(edge_probs)
|
|
889
|
+
transition = zeros((len(states), len(states)), symbolic=symbolic)
|
|
890
|
+
for transition_edge in graph.transitions():
|
|
891
|
+
source = index[transition_edge.source]
|
|
892
|
+
target = index[transition_edge.target]
|
|
893
|
+
transition[source, target] = as_prob(transition[source, target]) + as_prob(
|
|
894
|
+
transition_edge.data.get(ATTR_PROB, 0.0)
|
|
895
|
+
)
|
|
896
|
+
if symbolic:
|
|
897
|
+
pi = stationary_distribution_from_transition(transition)
|
|
898
|
+
return {states[position]: as_prob(pi[position]) for position in range(len(states))}
|
|
899
|
+
stationary = np.ones(len(states), dtype=float) / len(states)
|
|
900
|
+
for _ in range(20_000):
|
|
901
|
+
stationary = stationary @ np.asarray(transition, dtype=float)
|
|
902
|
+
return {states[position]: float(stationary[position]) for position in range(len(states))}
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _project_bidirectional_side(
|
|
906
|
+
graph: TransitionGraph,
|
|
907
|
+
joint_pi: Mapping[tuple[str, str], Any],
|
|
908
|
+
*,
|
|
909
|
+
project_forward: bool,
|
|
910
|
+
future_symbols: Mapping[str, Any] | None = None,
|
|
911
|
+
) -> EpsilonMachine:
|
|
912
|
+
"""Marginalize a hand-built bidirectional graph to an ε-machine presentation."""
|
|
913
|
+
from sofic.generators.prob import (
|
|
914
|
+
as_prob,
|
|
915
|
+
is_positive_mass,
|
|
916
|
+
simplify_prob,
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
coordinate = 0 if project_forward else 1
|
|
920
|
+
marginal: dict[str, Any] = {}
|
|
921
|
+
for pair, mass in joint_pi.items():
|
|
922
|
+
side_state = pair[coordinate]
|
|
923
|
+
if side_state in marginal:
|
|
924
|
+
marginal[side_state] = simplify_prob(as_prob(marginal[side_state]) + as_prob(mass))
|
|
925
|
+
else:
|
|
926
|
+
marginal[side_state] = as_prob(mass)
|
|
927
|
+
|
|
928
|
+
side_graph = TransitionGraph()
|
|
929
|
+
for state, mass in marginal.items():
|
|
930
|
+
if not is_positive_mass(mass):
|
|
931
|
+
continue
|
|
932
|
+
attrs: dict[str, Any] = {}
|
|
933
|
+
if future_symbols is not None and state in future_symbols:
|
|
934
|
+
attrs[ATTR_FUTURE_SYMBOL] = future_symbols[state]
|
|
935
|
+
side_graph.add_state(state, **attrs)
|
|
936
|
+
|
|
937
|
+
aggregated: dict[tuple[str, str, Any], Any] = {}
|
|
938
|
+
for pair, mass in joint_pi.items():
|
|
939
|
+
source = pair[coordinate]
|
|
940
|
+
source_mass = marginal.get(source, 0)
|
|
941
|
+
if not is_positive_mass(mass) or not is_positive_mass(source_mass):
|
|
942
|
+
continue
|
|
943
|
+
for transition in graph.out_transitions(pair):
|
|
944
|
+
symbol = transition.data.get(ATTR_EMISSION)
|
|
945
|
+
prob = as_prob(transition.data.get(ATTR_PROB, 0.0))
|
|
946
|
+
if symbol is None or not is_positive_mass(prob):
|
|
947
|
+
continue
|
|
948
|
+
target_state = transition.target[coordinate]
|
|
949
|
+
key = (source, target_state, symbol)
|
|
950
|
+
contrib = simplify_prob(as_prob(mass) * as_prob(prob) / as_prob(source_mass))
|
|
951
|
+
if key in aggregated:
|
|
952
|
+
aggregated[key] = simplify_prob(as_prob(aggregated[key]) + contrib)
|
|
953
|
+
else:
|
|
954
|
+
aggregated[key] = contrib
|
|
955
|
+
|
|
956
|
+
for (source, target, symbol), prob in aggregated.items():
|
|
957
|
+
if not is_positive_mass(prob):
|
|
958
|
+
continue
|
|
959
|
+
side_graph.add_transition(
|
|
960
|
+
source,
|
|
961
|
+
target,
|
|
962
|
+
**{ATTR_PROB: as_prob(prob), ATTR_EMISSION: symbol},
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
eps = EpsilonMachine(
|
|
966
|
+
graph=side_graph,
|
|
967
|
+
initial_distribution=marginal,
|
|
968
|
+
observation_alphabet=frozenset({0, 1}),
|
|
969
|
+
)
|
|
970
|
+
eps.validate_stochastic()
|
|
971
|
+
return eps
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def tent_map_misiurewicz_bidirectional_fig8(a: Any | None = None):
|
|
975
|
+
"""Hand-built supplement Fig.~8 bidirectional ε-machine."""
|
|
976
|
+
from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
|
|
977
|
+
from sofic.generators.prob import as_prob, is_symbolic
|
|
978
|
+
|
|
979
|
+
if a is None:
|
|
980
|
+
a = tent_map_misiurewicz_a()
|
|
981
|
+
forward = tent_map_misiurewicz_forward(a)
|
|
982
|
+
graph = TransitionGraph()
|
|
983
|
+
for source, target, symbol, prob in _tent_map_misiurewicz_fig8_edges(a):
|
|
984
|
+
if not graph.has_state(source):
|
|
985
|
+
graph.add_state(source)
|
|
986
|
+
if not graph.has_state(target):
|
|
987
|
+
graph.add_state(target)
|
|
988
|
+
graph.add_transition(
|
|
989
|
+
source,
|
|
990
|
+
target,
|
|
991
|
+
**{ATTR_PROB: as_prob(prob), ATTR_EMISSION: symbol},
|
|
992
|
+
)
|
|
993
|
+
joint_pi = _stationary_distribution_from_joint_graph(graph)
|
|
994
|
+
reverse_raw = _project_bidirectional_side(
|
|
995
|
+
graph,
|
|
996
|
+
joint_pi,
|
|
997
|
+
project_forward=False,
|
|
998
|
+
future_symbols={"E": 0, "F": 1, "G": 1, "H": -1},
|
|
999
|
+
)
|
|
1000
|
+
if is_symbolic(a):
|
|
1001
|
+
from sofic.generators.epsilon_machine import _row_normalized_presentation
|
|
1002
|
+
|
|
1003
|
+
reverse = _row_normalized_presentation(reverse_raw)
|
|
1004
|
+
else:
|
|
1005
|
+
try:
|
|
1006
|
+
reverse = EpsilonMachine.from_hmm(reverse_raw)
|
|
1007
|
+
except Exception:
|
|
1008
|
+
from sofic.generators.epsilon_machine import _row_normalized_presentation
|
|
1009
|
+
|
|
1010
|
+
reverse = _row_normalized_presentation(reverse_raw)
|
|
1011
|
+
_annotate_tent_map_misiurewicz_reverse_future_symbols(reverse)
|
|
1012
|
+
bidir = BidirectionalEpsilonMachine(
|
|
1013
|
+
graph=graph,
|
|
1014
|
+
initial_distribution=joint_pi,
|
|
1015
|
+
observation_alphabet=frozenset({0, 1}),
|
|
1016
|
+
forward_machine=forward,
|
|
1017
|
+
reverse_machine=reverse,
|
|
1018
|
+
)
|
|
1019
|
+
bidir._joint_pi = dict(joint_pi)
|
|
1020
|
+
bidir.validate()
|
|
1021
|
+
return bidir
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def tent_map_misiurewicz_bidirectional(a: float | None = None):
|
|
1025
|
+
"""Bidirectional ε-machine for the tent map at the Misiurewicz parameter."""
|
|
1026
|
+
return tent_map_misiurewicz_bidirectional_fig8(a)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def tent_map_misiurewicz_information_expected(a: Any | None = None) -> dict[str, Any]:
|
|
1030
|
+
"""Closed-form anatomy rates from James et al. (2013), supplement.
|
|
1031
|
+
|
|
1032
|
+
Returns floats when ``a`` is numeric, or sympy expressions when ``a`` is
|
|
1033
|
+
symbolic. The ephemeral rate is
|
|
1034
|
+
``r_μ = (1/4)*(3 - 2/(a+1) - 4/(a+2) + 9/(2a+3))``.
|
|
1035
|
+
|
|
1036
|
+
This is the kneading partition, i.e. the ``"c"`` member of the family of
|
|
1037
|
+
:func:`tent_map_misiurewicz_partition_information_expected`, which states the
|
|
1038
|
+
same rate reduced modulo ``a**3 = 2a + 2`` to ``(59 + 7a - 11a**2)/57``.
|
|
1039
|
+
"""
|
|
1040
|
+
from sofic.generators.prob import is_symbolic
|
|
1041
|
+
|
|
1042
|
+
if a is None:
|
|
1043
|
+
a = tent_map_misiurewicz_a()
|
|
1044
|
+
if is_symbolic(a):
|
|
1045
|
+
import sympy as sp
|
|
1046
|
+
|
|
1047
|
+
h_mu = sp.log(a, 2)
|
|
1048
|
+
r_mu = sp.Rational(1, 4) * (3 - 2 / (a + 1) - 4 / (a + 2) + 9 / (2 * a + 3))
|
|
1049
|
+
b_mu = sp.simplify(h_mu - r_mu)
|
|
1050
|
+
return {
|
|
1051
|
+
"bound_mu": b_mu,
|
|
1052
|
+
"ephemeral_mu": sp.simplify(r_mu),
|
|
1053
|
+
"entropy_rate": h_mu,
|
|
1054
|
+
}
|
|
1055
|
+
h_mu = math.log2(a)
|
|
1056
|
+
r_mu = 0.25 * (3.0 - 2.0 / (a + 1) - 4.0 / (a + 2) + 9.0 / (2 * a + 3))
|
|
1057
|
+
b_mu = h_mu - r_mu
|
|
1058
|
+
return {
|
|
1059
|
+
"bound_mu": b_mu,
|
|
1060
|
+
"ephemeral_mu": r_mu,
|
|
1061
|
+
"entropy_rate": h_mu,
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def ellison_fig9_reverse() -> EpsilonMachine:
|
|
1066
|
+
"""Reverse ε-machine from Ellison et al., arXiv:1107.2168, Fig.~9.
|
|
1067
|
+
|
|
1068
|
+
MSP-derived presentation used for Fig.~15 bidirectional pairing via Eq.~(15).
|
|
1069
|
+
"""
|
|
1070
|
+
from sofic.generators.reversal import time_reverse_stochastic
|
|
1071
|
+
|
|
1072
|
+
forward = ellison_fig9_forward()
|
|
1073
|
+
reverse = EpsilonMachine.from_hmm(time_reverse_stochastic(forward))
|
|
1074
|
+
reverse.validate()
|
|
1075
|
+
return reverse
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def ellison_fig15_bidirectional():
|
|
1079
|
+
"""Bidirectional ε-machine M± from Ellison et al., arXiv:1107.2168, Fig.~15.
|
|
1080
|
+
|
|
1081
|
+
Built from the separate forward and reverse presentations in Fig.~9 via
|
|
1082
|
+
Eq.~(15) in the same paper.
|
|
1083
|
+
"""
|
|
1084
|
+
from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
|
|
1085
|
+
|
|
1086
|
+
return BidirectionalEpsilonMachine.from_pair(
|
|
1087
|
+
ellison_fig9_forward(),
|
|
1088
|
+
ellison_fig9_reverse(),
|
|
1089
|
+
)
|