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,428 @@
|
|
|
1
|
+
"""Five-variable information-anatomy I-diagram over ``Pr(S⁺₀, S⁻₀, X₀, S⁺₁, S⁻₁)``.
|
|
2
|
+
|
|
3
|
+
The signed I-measure (:cite:`yeung1991new`) of the five bidirectional random
|
|
4
|
+
variables has ``2⁵ − 1 = 31`` atoms. Each atom is the *conditional
|
|
5
|
+
co-information* of the variables that are "inside" the region given the ones
|
|
6
|
+
that are "outside". For a nonempty subset ``S`` of the five random variables,
|
|
7
|
+
|
|
8
|
+
.. math::
|
|
9
|
+
|
|
10
|
+
a_S = I\\!\\left(X_i : i \\in S \\;\\middle|\\; X_j : j \\notin S\\right),
|
|
11
|
+
|
|
12
|
+
which reduces to a conditional entropy when ``|S| = 1`` and to a (possibly
|
|
13
|
+
negative) conditional co-information otherwise.
|
|
14
|
+
|
|
15
|
+
Unifilarity forces ten of the 31 atoms to vanish identically. Of the remaining
|
|
16
|
+
twenty-one *generically nonzero* atoms, fourteen are the named taxonomy atoms of
|
|
17
|
+
:cite:`jurgens2026taxonomy` Table II; the other seven are cancelling partners of
|
|
18
|
+
shielded four-variable zeros (Theorem A / A′ and the classical ``q_μ`` refinement)
|
|
19
|
+
that Table II omits.
|
|
20
|
+
|
|
21
|
+
:func:`information_diagram` computes every atom, classifies it into an anatomy
|
|
22
|
+
role, attaches the Jurgens taxonomy label when one exists, and reports named
|
|
23
|
+
totals.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from itertools import combinations
|
|
30
|
+
from typing import Any, Literal
|
|
31
|
+
|
|
32
|
+
# Anatomy variable ordering; matches ``bidirectional_step_distribution``.
|
|
33
|
+
_S_PLUS_0, _S_MINUS_0, _X_0, _S_PLUS_1, _S_MINUS_1 = 0, 1, 2, 3, 4
|
|
34
|
+
_ALL_INDICES: tuple[int, ...] = (_S_PLUS_0, _S_MINUS_0, _X_0, _S_PLUS_1, _S_MINUS_1)
|
|
35
|
+
|
|
36
|
+
#: Display names for the five random variables, indexed as in the step joint.
|
|
37
|
+
VARIABLE_NAMES: tuple[str, ...] = ("S⁺₀", "S⁻₀", "X₀", "S⁺₁", "S⁻₁")
|
|
38
|
+
|
|
39
|
+
#: Fixed anatomy roles, in the order atoms are laid out (never sorted by value).
|
|
40
|
+
ROLE_ORDER: tuple[str, ...] = (
|
|
41
|
+
"r_gauge", # rμ: X₀ only (transient / pure output relabeling)
|
|
42
|
+
"r_fwd", # rμ: X₀ + next forward state
|
|
43
|
+
"r_rev", # rμ: X₀ + previous reverse state
|
|
44
|
+
"r_joint", # rμ: X₀ + both next states
|
|
45
|
+
"b_plus", # b⁺μ: present ∩ future ∖ past (forward binding zone)
|
|
46
|
+
"b_minus", # b⁻μ: present ∩ past ∖ future (reverse binding zone)
|
|
47
|
+
"q_mu", # classical qμ = I[S⁺₀:X₀:S⁻₁] (present ∩ past ∩ future)
|
|
48
|
+
"sigma_mu", # σμ: past↔future information bypassing the present
|
|
49
|
+
"chi_plus", # χ⁺ crypticity atoms (t.χ⁺ / p.χ⁺)
|
|
50
|
+
"chi_minus", # χ⁻ crypticity atoms (t.χ⁻ / p.χ⁻)
|
|
51
|
+
"structure", # leftover state-structure (always zero under unifilarity)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
#: Colour / legend group for each fine role (several roles share a colour).
|
|
55
|
+
COLOR_GROUP: dict[str, str] = {
|
|
56
|
+
"r_gauge": "r_mu",
|
|
57
|
+
"r_fwd": "r_mu",
|
|
58
|
+
"r_rev": "r_mu",
|
|
59
|
+
"r_joint": "r_mu",
|
|
60
|
+
"b_plus": "b_plus",
|
|
61
|
+
"b_minus": "b_minus",
|
|
62
|
+
"q_mu": "q_mu",
|
|
63
|
+
"sigma_mu": "sigma_mu",
|
|
64
|
+
"chi_plus": "chi_plus",
|
|
65
|
+
"chi_minus": "chi_minus",
|
|
66
|
+
"structure": "structure",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#: Jurgens & Crutchfield (2026) Table II labels by Yeung-atom membership.
|
|
70
|
+
JURGENS_LABELS: dict[tuple[int, ...], str] = {
|
|
71
|
+
(2,): "t.rμ",
|
|
72
|
+
(1, 2): "p.r⁻μ",
|
|
73
|
+
(2, 3): "p.r⁺μ",
|
|
74
|
+
(1, 2, 3): "p.r±μ",
|
|
75
|
+
(2, 3, 4): "t.b⁺μ",
|
|
76
|
+
(1, 2, 3, 4): "p.b⁺μ",
|
|
77
|
+
(0, 1, 2): "t.b⁻μ",
|
|
78
|
+
(0, 1, 2, 3): "p.b⁻μ",
|
|
79
|
+
(0, 1, 2, 3, 4): "qμ",
|
|
80
|
+
(0, 1, 3, 4): "σμ",
|
|
81
|
+
(0,): "t.χ⁺",
|
|
82
|
+
(0, 3): "p.χ⁺",
|
|
83
|
+
(4,): "t.χ⁻",
|
|
84
|
+
(1, 4): "p.χ⁻",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#: Labels for the seven generically-nonzero atoms omitted from Table II.
|
|
88
|
+
#: These are the cancelling partners of shielded four-variable zeros.
|
|
89
|
+
EXTRA_LABELS: dict[tuple[int, ...], str] = {
|
|
90
|
+
(2, 4): "†b⁺μ gauge",
|
|
91
|
+
(1, 2, 4): "†b⁺μ rev",
|
|
92
|
+
(0, 2): "†b⁻μ gauge",
|
|
93
|
+
(0, 2, 3): "†b⁻μ fwd",
|
|
94
|
+
(0, 2, 4): "†qμ gauge",
|
|
95
|
+
(0, 1, 2, 4): "†qμ rev",
|
|
96
|
+
(0, 2, 3, 4): "†qμ fwd",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#: The 21 Yeung atoms that are not forced to zero by unifilarity / shielding
|
|
100
|
+
#: (Table II's fourteen plus the seven cancelling extras). Verified across the
|
|
101
|
+
#: structural-ephemeral zoo.
|
|
102
|
+
GENERICALLY_NONZERO: frozenset[tuple[int, ...]] = frozenset(
|
|
103
|
+
{
|
|
104
|
+
*JURGENS_LABELS,
|
|
105
|
+
*EXTRA_LABELS,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
#: Zone symbol for plot / legend naming.
|
|
110
|
+
_ZONE_SYMBOL: dict[str, str | None] = {
|
|
111
|
+
"r_gauge": "rμ",
|
|
112
|
+
"r_fwd": "rμ",
|
|
113
|
+
"r_rev": "rμ",
|
|
114
|
+
"r_joint": "rμ",
|
|
115
|
+
"b_plus": "b⁺μ",
|
|
116
|
+
"b_minus": "b⁻μ",
|
|
117
|
+
"q_mu": "qμ",
|
|
118
|
+
"sigma_mu": "σμ",
|
|
119
|
+
"chi_plus": "χ⁺",
|
|
120
|
+
"chi_minus": "χ⁻",
|
|
121
|
+
"structure": None,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#: Maps each role to the ``totals`` key holding its aggregate value.
|
|
125
|
+
ROLE_TOTAL_KEY: dict[str, str] = {
|
|
126
|
+
"r_gauge": "r_gauge",
|
|
127
|
+
"r_fwd": "r_fwd",
|
|
128
|
+
"r_rev": "r_rev",
|
|
129
|
+
"r_joint": "r_joint",
|
|
130
|
+
"b_plus": "b_plus",
|
|
131
|
+
"b_minus": "b_minus",
|
|
132
|
+
"q_mu": "q_mu",
|
|
133
|
+
"sigma_mu": "sigma_mu",
|
|
134
|
+
"chi_plus": "chi_plus",
|
|
135
|
+
"chi_minus": "chi_minus",
|
|
136
|
+
"structure": "structure",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _require_dit() -> Any:
|
|
141
|
+
from sofic.generators.measures import require_dit
|
|
142
|
+
|
|
143
|
+
return require_dit("information diagrams")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _branch_name(indices: tuple[int, ...]) -> str:
|
|
147
|
+
next_fwd = _S_PLUS_1 in indices
|
|
148
|
+
prev_rev = _S_MINUS_0 in indices
|
|
149
|
+
if next_fwd and prev_rev:
|
|
150
|
+
return "joint"
|
|
151
|
+
if next_fwd:
|
|
152
|
+
return "fwd"
|
|
153
|
+
if prev_rev:
|
|
154
|
+
return "rev"
|
|
155
|
+
return "gauge"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _classify(indices: tuple[int, ...]) -> str:
|
|
159
|
+
"""Assign an I-diagram atom to exactly one anatomy role by its membership."""
|
|
160
|
+
inside = set(indices)
|
|
161
|
+
present = _X_0 in inside
|
|
162
|
+
past = _S_PLUS_0 in inside
|
|
163
|
+
future = _S_MINUS_1 in inside
|
|
164
|
+
if present:
|
|
165
|
+
if past and future:
|
|
166
|
+
return "q_mu"
|
|
167
|
+
if future:
|
|
168
|
+
return "b_plus"
|
|
169
|
+
if past:
|
|
170
|
+
return "b_minus"
|
|
171
|
+
branch = _branch_name(indices)
|
|
172
|
+
return {
|
|
173
|
+
"gauge": "r_gauge",
|
|
174
|
+
"fwd": "r_fwd",
|
|
175
|
+
"rev": "r_rev",
|
|
176
|
+
"joint": "r_joint",
|
|
177
|
+
}[branch]
|
|
178
|
+
if past and future:
|
|
179
|
+
return "sigma_mu"
|
|
180
|
+
if past:
|
|
181
|
+
return "chi_plus"
|
|
182
|
+
if future:
|
|
183
|
+
return "chi_minus"
|
|
184
|
+
return "structure"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _atom_symbol(indices: tuple[int, ...]) -> str | None:
|
|
188
|
+
"""A ``{zone} {branch}`` anatomy name, e.g. ``rμ gauge`` or ``χ⁺ transient``."""
|
|
189
|
+
role = _classify(indices)
|
|
190
|
+
zone = _ZONE_SYMBOL[role]
|
|
191
|
+
if zone is None:
|
|
192
|
+
return None
|
|
193
|
+
if role in ("chi_plus", "chi_minus"):
|
|
194
|
+
# Crypticity: transient = singleton state, persistent = correlated with next.
|
|
195
|
+
persistent = (_S_PLUS_1 in indices) if role == "chi_plus" else (_S_MINUS_0 in indices)
|
|
196
|
+
return f"{zone} {'persistent' if persistent else 'transient'}"
|
|
197
|
+
if role == "sigma_mu":
|
|
198
|
+
return f"{zone} {_branch_name(indices)}"
|
|
199
|
+
return f"{zone} {_branch_name(indices)}"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _taxonomy_label(indices: tuple[int, ...]) -> str | None:
|
|
203
|
+
"""Jurgens Table II label, or a †-marked extra label; ``None`` if always zero."""
|
|
204
|
+
if indices in JURGENS_LABELS:
|
|
205
|
+
return JURGENS_LABELS[indices]
|
|
206
|
+
if indices in EXTRA_LABELS:
|
|
207
|
+
return EXTRA_LABELS[indices]
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _ordering_key(indices: tuple[int, ...]) -> tuple[int, int, tuple[int, ...]]:
|
|
212
|
+
return (ROLE_ORDER.index(_classify(indices)), len(indices), indices)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _coerce(dist: Any, value: Any) -> Any:
|
|
216
|
+
"""Return a float for numeric distributions, the raw expression if symbolic."""
|
|
217
|
+
if hasattr(dist, "is_symbolic") and dist.is_symbolic():
|
|
218
|
+
return value
|
|
219
|
+
return float(value)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _is_zero(value: Any, tol: float) -> bool:
|
|
223
|
+
try:
|
|
224
|
+
return abs(float(value)) <= tol
|
|
225
|
+
except (TypeError, ValueError):
|
|
226
|
+
return False # symbolic expression: keep it
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class IDiagramAtom:
|
|
231
|
+
"""One atom of the five-variable information diagram.
|
|
232
|
+
|
|
233
|
+
Attributes:
|
|
234
|
+
indices: Random-variable indices that are *inside* the region.
|
|
235
|
+
variables: The matching :data:`VARIABLE_NAMES`.
|
|
236
|
+
value: The atom's I-measure (a conditional co-information); ``float`` for
|
|
237
|
+
numeric machines, a sympy expression for symbolic ones.
|
|
238
|
+
role: One of :data:`ROLE_ORDER`.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
indices: tuple[int, ...]
|
|
242
|
+
variables: tuple[str, ...]
|
|
243
|
+
value: Any
|
|
244
|
+
role: str
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def label(self) -> str:
|
|
248
|
+
"""Set-builder label, e.g. ``{X₀,S⁺₁}``."""
|
|
249
|
+
return "{" + ",".join(self.variables) + "}"
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def conditional_expression(self) -> str:
|
|
253
|
+
"""The atom's I-measure as a conditional co-information, e.g.
|
|
254
|
+
``I[X₀:S⁺₁|S⁺₀,S⁻₀,S⁻₁]`` (a conditional entropy ``H[…|…]`` when a
|
|
255
|
+
single variable is inside). This is the term's exact name."""
|
|
256
|
+
inside = ":".join(self.variables)
|
|
257
|
+
outside = ",".join(VARIABLE_NAMES[i] for i in _ALL_INDICES if i not in self.indices)
|
|
258
|
+
head = "H" if len(self.indices) == 1 else "I"
|
|
259
|
+
body = inside if not outside else f"{inside}|{outside}"
|
|
260
|
+
return f"{head}[{body}]"
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def symbol(self) -> str | None:
|
|
264
|
+
"""A ``{zone} {branch}`` anatomy name (e.g. ``rμ gauge``, ``χ⁺ transient``)."""
|
|
265
|
+
return _atom_symbol(self.indices)
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def jurgens_label(self) -> str | None:
|
|
269
|
+
"""Taxonomy label from :cite:`jurgens2026taxonomy` Table II, or a
|
|
270
|
+
``†``-marked extra for the seven cancelling partners omitted there."""
|
|
271
|
+
return _taxonomy_label(self.indices)
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def color_group(self) -> str:
|
|
275
|
+
"""Coarse colour group (``r_mu``, ``b_plus``, ``b_minus``, …)."""
|
|
276
|
+
return COLOR_GROUP[self.role]
|
|
277
|
+
|
|
278
|
+
def value_float(self) -> float:
|
|
279
|
+
"""The atom value as a float (raises for symbolic values)."""
|
|
280
|
+
return float(self.value)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass
|
|
284
|
+
class InformationDiagram:
|
|
285
|
+
"""The full five-variable information diagram with anatomy roles and totals.
|
|
286
|
+
|
|
287
|
+
Attributes:
|
|
288
|
+
atoms: Retained atoms in fixed role order (see ``atoms`` argument of
|
|
289
|
+
:func:`information_diagram`).
|
|
290
|
+
variable_names: The five variable display names.
|
|
291
|
+
totals: Named anatomy quantities and per-role sums.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
atoms: list[IDiagramAtom]
|
|
295
|
+
variable_names: tuple[str, ...]
|
|
296
|
+
totals: dict[str, Any]
|
|
297
|
+
|
|
298
|
+
def by_role(self) -> dict[str, list[IDiagramAtom]]:
|
|
299
|
+
"""Group the retained atoms by role, in :data:`ROLE_ORDER`."""
|
|
300
|
+
groups: dict[str, list[IDiagramAtom]] = {role: [] for role in ROLE_ORDER}
|
|
301
|
+
for atom in self.atoms:
|
|
302
|
+
groups[atom.role].append(atom)
|
|
303
|
+
return groups
|
|
304
|
+
|
|
305
|
+
def __repr__(self) -> str: # pragma: no cover - cosmetic
|
|
306
|
+
t = self.totals
|
|
307
|
+
return (
|
|
308
|
+
"InformationDiagram("
|
|
309
|
+
f"atoms={len(self.atoms)}, "
|
|
310
|
+
f"h_mu={t['h_mu']:.4f}, r_mu={t['r_mu']:.4f}, "
|
|
311
|
+
f"b_mu={t['b_mu']:.4f}, sigma_mu={t['sigma_mu']:.4f})"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _resolve_step_distribution(source: Any) -> Any:
|
|
316
|
+
"""Extract the five-variable step joint from a machine or pass a distribution."""
|
|
317
|
+
if hasattr(source, "step_distribution"):
|
|
318
|
+
return source.step_distribution()
|
|
319
|
+
if hasattr(source, "to_bidirectional"):
|
|
320
|
+
return source.to_bidirectional().step_distribution()
|
|
321
|
+
return source
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _compute_totals(records: list[tuple[tuple[int, ...], Any]]) -> dict[str, Any]:
|
|
325
|
+
role_sum: dict[str, Any] = dict.fromkeys(ROLE_ORDER, 0)
|
|
326
|
+
for indices, value in records:
|
|
327
|
+
role_sum[_classify(indices)] += value
|
|
328
|
+
r_mu = role_sum["r_gauge"] + role_sum["r_fwd"] + role_sum["r_rev"] + role_sum["r_joint"]
|
|
329
|
+
b_plus = role_sum["b_plus"]
|
|
330
|
+
b_minus = role_sum["b_minus"]
|
|
331
|
+
q_mu = role_sum["q_mu"]
|
|
332
|
+
totals: dict[str, Any] = dict(role_sum)
|
|
333
|
+
totals.update(
|
|
334
|
+
{
|
|
335
|
+
"r_mu": r_mu,
|
|
336
|
+
"b_mu": b_plus, # stationary: b⁺μ = b⁻μ = b_μ
|
|
337
|
+
"b_plus": b_plus,
|
|
338
|
+
"b_minus": b_minus,
|
|
339
|
+
"q_mu": q_mu,
|
|
340
|
+
"sigma_mu": role_sum["sigma_mu"],
|
|
341
|
+
"chi_plus": role_sum["chi_plus"],
|
|
342
|
+
"chi_minus": role_sum["chi_minus"],
|
|
343
|
+
# Classical predicted information I[X₀:S⁺₀] = b⁻ zone + qμ zone.
|
|
344
|
+
"rho_mu": b_minus + q_mu,
|
|
345
|
+
"h_mu": r_mu + b_plus,
|
|
346
|
+
"H[X0]": r_mu + b_plus + b_minus + q_mu,
|
|
347
|
+
"h_imc": b_plus + role_sum["r_fwd"] + role_sum["r_joint"],
|
|
348
|
+
"h_imc_reverse": b_plus + role_sum["r_rev"] + role_sum["r_joint"],
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
return totals
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def information_diagram(
|
|
355
|
+
source: Any,
|
|
356
|
+
*,
|
|
357
|
+
show_zero: bool = False,
|
|
358
|
+
atoms: Literal["process", "generic", "all"] | None = None,
|
|
359
|
+
tol: float = 1e-9,
|
|
360
|
+
) -> InformationDiagram:
|
|
361
|
+
"""Compute the five-variable information-anatomy I-diagram.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
source: A :class:`~sofic.generators.bidirectional_epsilon_machine.BidirectionalEpsilonMachine`,
|
|
365
|
+
an :class:`~sofic.generators.epsilon_machine.EpsilonMachine` (converted
|
|
366
|
+
via ``to_bidirectional``), or a five-variable ``dit`` distribution
|
|
367
|
+
whose random variables are ordered ``(S⁺₀, S⁻₀, X₀, S⁺₁, S⁻₁)``.
|
|
368
|
+
show_zero: Deprecated alias — if ``True`` and ``atoms`` is omitted,
|
|
369
|
+
keep every atom (``atoms="all"``). Prefer the ``atoms`` argument.
|
|
370
|
+
atoms: Which atoms to retain:
|
|
371
|
+
|
|
372
|
+
* ``"process"`` (default) — only numerically nonzero atoms for this
|
|
373
|
+
process;
|
|
374
|
+
* ``"generic"`` — the 21 generically nonzero membership sets
|
|
375
|
+
(Table II's 14 plus the 7 cancelling extras), including zeros for
|
|
376
|
+
this process;
|
|
377
|
+
* ``"all"`` — all 31 nonempty Yeung atoms.
|
|
378
|
+
tol: Magnitude below which a numeric atom is treated as zero.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
An :class:`InformationDiagram` with the retained atoms in fixed
|
|
382
|
+
:data:`ROLE_ORDER` layout and the named anatomy ``totals``.
|
|
383
|
+
"""
|
|
384
|
+
_require_dit()
|
|
385
|
+
from dit.multivariate import coinformation
|
|
386
|
+
|
|
387
|
+
if atoms is None:
|
|
388
|
+
atoms = "all" if show_zero else "process"
|
|
389
|
+
if atoms not in ("process", "generic", "all"):
|
|
390
|
+
raise ValueError(f"atoms must be 'process', 'generic', or 'all'; got {atoms!r}")
|
|
391
|
+
|
|
392
|
+
dist = _resolve_step_distribution(source)
|
|
393
|
+
length = dist.outcome_length() if hasattr(dist, "outcome_length") else None
|
|
394
|
+
if length is not None and length != len(_ALL_INDICES):
|
|
395
|
+
raise ValueError(
|
|
396
|
+
f"information_diagram expects a 5-variable step distribution "
|
|
397
|
+
f"(S⁺₀, S⁻₀, X₀, S⁺₁, S⁻₁); got outcome length {length}."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
records: list[tuple[tuple[int, ...], Any]] = []
|
|
401
|
+
for size in range(1, len(_ALL_INDICES) + 1):
|
|
402
|
+
for subset in combinations(_ALL_INDICES, size):
|
|
403
|
+
crvs = [i for i in _ALL_INDICES if i not in subset]
|
|
404
|
+
value = coinformation(dist, rvs=[[i] for i in subset], crvs=crvs)
|
|
405
|
+
records.append((subset, _coerce(dist, value)))
|
|
406
|
+
|
|
407
|
+
records.sort(key=lambda record: _ordering_key(record[0]))
|
|
408
|
+
|
|
409
|
+
retained: list[IDiagramAtom] = []
|
|
410
|
+
for subset, value in records:
|
|
411
|
+
if atoms == "process" and _is_zero(value, tol):
|
|
412
|
+
continue
|
|
413
|
+
if atoms == "generic" and subset not in GENERICALLY_NONZERO:
|
|
414
|
+
continue
|
|
415
|
+
retained.append(
|
|
416
|
+
IDiagramAtom(
|
|
417
|
+
indices=subset,
|
|
418
|
+
variables=tuple(VARIABLE_NAMES[i] for i in subset),
|
|
419
|
+
value=value,
|
|
420
|
+
role=_classify(subset),
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return InformationDiagram(
|
|
425
|
+
atoms=retained,
|
|
426
|
+
variable_names=VARIABLE_NAMES,
|
|
427
|
+
totals=_compute_totals(records),
|
|
428
|
+
)
|