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,257 @@
|
|
|
1
|
+
"""Information measures for stochastic generators via dit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from sofic.generators.base import HiddenMarkovModel, QuasiStochasticModel, StochasticModel
|
|
10
|
+
from sofic.generators.markov import MarkovChain
|
|
11
|
+
from sofic.graph import ATTR_EMISSION, ATTR_PROB
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def require_dit(feature: str = "entropy measures") -> Any:
|
|
15
|
+
"""Import and return the :mod:`dit` package, or raise a helpful error.
|
|
16
|
+
|
|
17
|
+
``feature`` names the capability requiring dit and is interpolated into the
|
|
18
|
+
error message when the optional dependency is missing.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
import dit
|
|
22
|
+
except ImportError as exc:
|
|
23
|
+
raise ImportError(f"dit is required for {feature}; install with `pip install dit`") from exc
|
|
24
|
+
return dit
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Backwards-compatible internal alias.
|
|
28
|
+
_require_dit = require_dit
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dit_state_label(state: Any) -> Any:
|
|
32
|
+
"""Return a dit-safe single-symbol label for a machine state.
|
|
33
|
+
|
|
34
|
+
``dit.Distribution`` treats each outcome as a sequence of random-variable
|
|
35
|
+
values, so a tuple-valued state (e.g. an edge-machine state like
|
|
36
|
+
``("A", "0", "A")``) is misread as multi-dimensional coordinate data and
|
|
37
|
+
raises ``MissingDimensionsError``. Tuple states are encoded to a lossless
|
|
38
|
+
string via :func:`sofic.generators.edge_machine.edge_state_label` (invert
|
|
39
|
+
with ``parse_edge_state_label``); scalar states pass through unchanged.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(state, tuple):
|
|
42
|
+
from sofic.generators.edge_machine import edge_state_label
|
|
43
|
+
|
|
44
|
+
return edge_state_label(state)
|
|
45
|
+
return state
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def state_distribution(model: StochasticModel) -> Any:
|
|
49
|
+
"""Return the stationary state law as a ``dit.Distribution``.
|
|
50
|
+
|
|
51
|
+
States are emitted as dit-safe labels (see :func:`dit_state_label`): scalar
|
|
52
|
+
states are preserved verbatim, tuple states are encoded to a lossless string.
|
|
53
|
+
"""
|
|
54
|
+
dit = _require_dit()
|
|
55
|
+
idx = model.reindex()
|
|
56
|
+
pi = model.stationary_distribution()
|
|
57
|
+
outcomes = [(dit_state_label(idx.state(i)),) for i in range(len(idx))]
|
|
58
|
+
from sofic.generators.prob import as_prob, has_symbolic, simplify_prob
|
|
59
|
+
|
|
60
|
+
probs = [as_prob(pi[i]) for i in range(len(idx))]
|
|
61
|
+
if has_symbolic(probs):
|
|
62
|
+
from dit.symbolic import symbolic_distribution
|
|
63
|
+
|
|
64
|
+
return symbolic_distribution(outcomes, [simplify_prob(p) for p in probs])
|
|
65
|
+
return dit.Distribution(outcomes, [float(p) for p in probs])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def state_entropy(model: StochasticModel) -> Any:
|
|
69
|
+
"""Shannon entropy of the stationary state distribution in bits."""
|
|
70
|
+
dit = _require_dit()
|
|
71
|
+
dist = state_distribution(model)
|
|
72
|
+
value = dit.shannon.entropy(dist)
|
|
73
|
+
if hasattr(dist, "is_symbolic") and dist.is_symbolic():
|
|
74
|
+
return value
|
|
75
|
+
return float(value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def joint_block_distribution(
|
|
79
|
+
generator: HiddenMarkovModel,
|
|
80
|
+
history_length: int = 1,
|
|
81
|
+
) -> Any:
|
|
82
|
+
"""Build a ``dit.Distribution`` over observed emission blocks.
|
|
83
|
+
|
|
84
|
+
``history_length`` counts symbols before the present symbol, so the emitted
|
|
85
|
+
block length is ``history_length + 1``.
|
|
86
|
+
"""
|
|
87
|
+
from itertools import product
|
|
88
|
+
|
|
89
|
+
from sofic.generators.hmm_inference import _stationary_emission_tensors
|
|
90
|
+
|
|
91
|
+
dit = _require_dit()
|
|
92
|
+
# Blocks of a stationary process are weighted by the stationary state law, not
|
|
93
|
+
# the model's initial distribution (which may describe only the transient).
|
|
94
|
+
pi, joint = _stationary_emission_tensors(generator)
|
|
95
|
+
|
|
96
|
+
symbol_list = sorted(generator.observation_alphabet, key=repr)
|
|
97
|
+
block_length = max(1, history_length + 1)
|
|
98
|
+
ones = np.ones(len(pi), dtype=float)
|
|
99
|
+
outcomes = list(product(symbol_list, repeat=block_length))
|
|
100
|
+
probs = []
|
|
101
|
+
for outcome in outcomes:
|
|
102
|
+
mass = pi.copy()
|
|
103
|
+
for symbol in outcome:
|
|
104
|
+
mass = mass @ joint.get(symbol, np.zeros((len(pi), len(pi)), dtype=float))
|
|
105
|
+
probs.append(float(mass @ ones))
|
|
106
|
+
|
|
107
|
+
total = sum(probs)
|
|
108
|
+
if total > 0.0:
|
|
109
|
+
probs = [p / total for p in probs]
|
|
110
|
+
return dit.Distribution(outcomes, probs)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _entropy_rate_from_transitions(
|
|
114
|
+
model: StochasticModel,
|
|
115
|
+
pi: np.ndarray,
|
|
116
|
+
idx: Any,
|
|
117
|
+
) -> Any:
|
|
118
|
+
"""Entropy rate from edge probabilities when symbol-labeled joint mass is absent.
|
|
119
|
+
|
|
120
|
+
Accepts any :class:`StochasticModel` (visible Markov chain or hidden Markov
|
|
121
|
+
model); the target state stands in as the emitted symbol when no emission is set.
|
|
122
|
+
"""
|
|
123
|
+
dit = _require_dit()
|
|
124
|
+
from sofic.generators.prob import (
|
|
125
|
+
as_prob,
|
|
126
|
+
has_symbolic,
|
|
127
|
+
is_positive_mass,
|
|
128
|
+
is_symbolic,
|
|
129
|
+
simplify_prob,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
symbolic = pi.dtype == object or has_symbolic(pi.ravel())
|
|
133
|
+
rate: Any = 0 if symbolic else 0.0
|
|
134
|
+
for state in idx.states:
|
|
135
|
+
i = idx.index(state)
|
|
136
|
+
outgoing = list(model.graph.out_transitions(state))
|
|
137
|
+
if not outgoing:
|
|
138
|
+
continue
|
|
139
|
+
targets: list[Any] = []
|
|
140
|
+
probs: list[Any] = []
|
|
141
|
+
for transition in outgoing:
|
|
142
|
+
prob = as_prob(transition.data.get(ATTR_PROB, 0.0))
|
|
143
|
+
if not is_positive_mass(prob):
|
|
144
|
+
continue
|
|
145
|
+
emission = transition.data.get(ATTR_EMISSION)
|
|
146
|
+
target = (transition.target, emission) if emission is not None else transition.target
|
|
147
|
+
targets.append(dit_state_label(target))
|
|
148
|
+
probs.append(prob)
|
|
149
|
+
if not probs:
|
|
150
|
+
continue
|
|
151
|
+
if has_symbolic(probs) or symbolic:
|
|
152
|
+
from dit.symbolic import symbolic_distribution
|
|
153
|
+
|
|
154
|
+
conditional = symbolic_distribution(targets, [simplify_prob(p) for p in probs])
|
|
155
|
+
contrib = as_prob(pi[i]) * dit.shannon.entropy(conditional)
|
|
156
|
+
rate = simplify_prob(as_prob(rate) + as_prob(contrib))
|
|
157
|
+
else:
|
|
158
|
+
conditional = dit.Distribution(targets, [float(p) for p in probs])
|
|
159
|
+
rate += float(pi[i] * dit.shannon.entropy(conditional))
|
|
160
|
+
if is_symbolic(rate):
|
|
161
|
+
return simplify_prob(rate)
|
|
162
|
+
return float(rate)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def entropy_rate_hmm(hmm: HiddenMarkovModel) -> Any:
|
|
166
|
+
"""Shannon entropy rate for unifilar hidden Markov presentations.
|
|
167
|
+
|
|
168
|
+
Returns a sympy :class:`~sympy.Expr` when the stationary law or emission
|
|
169
|
+
tensors are symbolic; otherwise a Python ``float``.
|
|
170
|
+
"""
|
|
171
|
+
from sofic.generators.hmm_inference import _emission_transition_tensors
|
|
172
|
+
from sofic.generators.prob import (
|
|
173
|
+
array_sum,
|
|
174
|
+
as_prob,
|
|
175
|
+
has_symbolic,
|
|
176
|
+
is_positive_mass,
|
|
177
|
+
is_symbolic,
|
|
178
|
+
simplify_prob,
|
|
179
|
+
sum_probs,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
is_unifilar = getattr(hmm, "is_unifilar", None)
|
|
183
|
+
if is_unifilar is None or not is_unifilar():
|
|
184
|
+
raise NotImplementedError("entropy_rate_hmm is only exact for unifilar HMM presentations")
|
|
185
|
+
|
|
186
|
+
dit = _require_dit()
|
|
187
|
+
idx = hmm.reindex()
|
|
188
|
+
pi = hmm.stationary_distribution()
|
|
189
|
+
_, joint = _emission_transition_tensors(hmm)
|
|
190
|
+
|
|
191
|
+
symbolic = pi.dtype == object or has_symbolic(pi.ravel())
|
|
192
|
+
if not symbolic:
|
|
193
|
+
symbolic = any(matrix.dtype == object or has_symbolic(matrix.ravel()) for matrix in joint.values())
|
|
194
|
+
|
|
195
|
+
# Emit dit-safe state labels so tuple-valued states (e.g. edge-machine
|
|
196
|
+
# states like ("A", "0", "A")) do not break dit.Distribution.
|
|
197
|
+
outcomes: list[tuple[Any, Any]] = []
|
|
198
|
+
probs: list[Any] = []
|
|
199
|
+
for state in idx.states:
|
|
200
|
+
i = idx.index(state)
|
|
201
|
+
label = dit_state_label(state)
|
|
202
|
+
for symbol, matrix in joint.items():
|
|
203
|
+
row_mass = as_prob(pi[i]) * array_sum(matrix[i])
|
|
204
|
+
row_mass = simplify_prob(row_mass) if symbolic or is_symbolic(row_mass) else float(row_mass)
|
|
205
|
+
if not is_positive_mass(row_mass):
|
|
206
|
+
continue
|
|
207
|
+
outcomes.append((label, symbol))
|
|
208
|
+
probs.append(row_mass)
|
|
209
|
+
|
|
210
|
+
if not probs:
|
|
211
|
+
return (
|
|
212
|
+
entropy_rate_markov(hmm) if isinstance(hmm, MarkovChain) else _entropy_rate_from_transitions(hmm, pi, idx)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
total = sum_probs(probs)
|
|
216
|
+
state_dist = state_distribution(hmm)
|
|
217
|
+
if symbolic or has_symbolic(probs) or is_symbolic(total):
|
|
218
|
+
from dit.symbolic import symbolic_distribution
|
|
219
|
+
|
|
220
|
+
joint_dist = symbolic_distribution(
|
|
221
|
+
outcomes,
|
|
222
|
+
[simplify_prob(as_prob(p) / as_prob(total)) for p in probs],
|
|
223
|
+
)
|
|
224
|
+
return simplify_prob(as_prob(dit.shannon.entropy(joint_dist)) - as_prob(dit.shannon.entropy(state_dist)))
|
|
225
|
+
joint_dist = dit.Distribution(outcomes, [float(p) / float(total) for p in probs])
|
|
226
|
+
return float(dit.shannon.entropy(joint_dist) - dit.shannon.entropy(state_dist))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def entropy_rate_markov(chain: MarkovChain) -> Any:
|
|
230
|
+
"""Shannon entropy rate of a visible Markov chain in bits.
|
|
231
|
+
|
|
232
|
+
A visible Markov chain has no separate emissions, so its entropy rate is the
|
|
233
|
+
conditional-transition entropy computed by :func:`_entropy_rate_from_transitions`
|
|
234
|
+
(which treats the target state as the emitted symbol when no emission is set).
|
|
235
|
+
"""
|
|
236
|
+
idx = chain.reindex()
|
|
237
|
+
pi = chain.stationary_distribution()
|
|
238
|
+
return _entropy_rate_from_transitions(chain, pi, idx)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def collision_entropy(quasi_model: QuasiStochasticModel) -> float:
|
|
242
|
+
"""Second Renyi entropy rate from quasi transition matrices."""
|
|
243
|
+
matrices = quasi_model.transition_matrices()
|
|
244
|
+
pi = quasi_model.stationary_quasidistribution()
|
|
245
|
+
total = 0.0
|
|
246
|
+
for matrix in matrices.values():
|
|
247
|
+
total += float(pi @ (matrix @ matrix) @ np.ones(len(pi)))
|
|
248
|
+
if total <= 0.0:
|
|
249
|
+
return 0.0
|
|
250
|
+
return float(-np.log(total))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def process_negativity(quasi_model: QuasiStochasticModel) -> float:
|
|
254
|
+
"""Negativity proxy from the stationary quasidistribution."""
|
|
255
|
+
pi = quasi_model.stationary_quasidistribution()
|
|
256
|
+
positive = np.maximum(pi, 0.0)
|
|
257
|
+
return float(np.sum(np.abs(pi - positive)))
|