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.
Files changed (150) hide show
  1. sofic/__init__.py +185 -0
  2. sofic/automata/__init__.py +207 -0
  3. sofic/automata/_config_simulation.py +40 -0
  4. sofic/automata/active.py +611 -0
  5. sofic/automata/alergia.py +222 -0
  6. sofic/automata/algorithms.py +376 -0
  7. sofic/automata/atomaton.py +58 -0
  8. sofic/automata/base.py +161 -0
  9. sofic/automata/buchi.py +23 -0
  10. sofic/automata/buchi_simulation.py +67 -0
  11. sofic/automata/canonical_dual.py +18 -0
  12. sofic/automata/canonical_extraction.py +122 -0
  13. sofic/automata/dfa.py +85 -0
  14. sofic/automata/dfasat.py +195 -0
  15. sofic/automata/edsm.py +219 -0
  16. sofic/automata/enumeration.py +44 -0
  17. sofic/automata/icdfa.py +421 -0
  18. sofic/automata/idfa.py +363 -0
  19. sofic/automata/languages/__init__.py +39 -0
  20. sofic/automata/languages/_quotient_utils.py +64 -0
  21. sofic/automata/languages/atoms.py +31 -0
  22. sofic/automata/languages/automaton_ops.py +243 -0
  23. sofic/automata/languages/base.py +67 -0
  24. sofic/automata/languages/operations.py +78 -0
  25. sofic/automata/languages/quotients.py +66 -0
  26. sofic/automata/languages/residuals.py +25 -0
  27. sofic/automata/learning.py +79 -0
  28. sofic/automata/nfa.py +39 -0
  29. sofic/automata/nwa.py +343 -0
  30. sofic/automata/nwa_simulation.py +56 -0
  31. sofic/automata/observation.py +40 -0
  32. sofic/automata/papni.py +301 -0
  33. sofic/automata/regex.py +128 -0
  34. sofic/automata/rfsa.py +35 -0
  35. sofic/automata/rpni.py +193 -0
  36. sofic/automata/subsequential.py +201 -0
  37. sofic/automata/transducer_operations.py +350 -0
  38. sofic/automata/transducer_simulation.py +150 -0
  39. sofic/automata/transducers.py +365 -0
  40. sofic/automata/unifilar.py +107 -0
  41. sofic/automata/vpa.py +1373 -0
  42. sofic/automata/vpa_simulation.py +53 -0
  43. sofic/base.py +153 -0
  44. sofic/core.py +47 -0
  45. sofic/examples/__init__.py +86 -0
  46. sofic/examples/epsilon_machines.py +1089 -0
  47. sofic/examples/processes.py +1491 -0
  48. sofic/examples/shifts.py +144 -0
  49. sofic/exceptions.py +33 -0
  50. sofic/generators/__init__.py +115 -0
  51. sofic/generators/_word_measures.py +94 -0
  52. sofic/generators/alternative_complexity.py +104 -0
  53. sofic/generators/base.py +327 -0
  54. sofic/generators/bidirectional_construction.py +717 -0
  55. sofic/generators/bidirectional_epsilon_machine.py +689 -0
  56. sofic/generators/block_convergence.py +668 -0
  57. sofic/generators/block_entropy.py +578 -0
  58. sofic/generators/channel_measures.py +75 -0
  59. sofic/generators/conversions.py +182 -0
  60. sofic/generators/directional_flow.py +245 -0
  61. sofic/generators/edge_emissions.py +36 -0
  62. sofic/generators/edge_machine.py +178 -0
  63. sofic/generators/epsilon_construction.py +193 -0
  64. sofic/generators/epsilon_inference.py +703 -0
  65. sofic/generators/epsilon_machine.py +557 -0
  66. sofic/generators/epsilon_transducer.py +168 -0
  67. sofic/generators/epsilon_transducer_construction.py +185 -0
  68. sofic/generators/epsilon_transducer_inference.py +499 -0
  69. sofic/generators/hmm_inference.py +719 -0
  70. sofic/generators/information_diagram.py +428 -0
  71. sofic/generators/lumping.py +447 -0
  72. sofic/generators/markov.py +100 -0
  73. sofic/generators/mealy.py +156 -0
  74. sofic/generators/measures.py +257 -0
  75. sofic/generators/minimal_generative_model.py +821 -0
  76. sofic/generators/mixed_state.py +250 -0
  77. sofic/generators/mixed_state_construction.py +163 -0
  78. sofic/generators/moore.py +75 -0
  79. sofic/generators/nmachine.py +78 -0
  80. sofic/generators/nmachine_construction.py +70 -0
  81. sofic/generators/pfa.py +100 -0
  82. sofic/generators/prob.py +291 -0
  83. sofic/generators/process_equivalence.py +207 -0
  84. sofic/generators/quasi_inference.py +74 -0
  85. sofic/generators/quasi_realization.py +97 -0
  86. sofic/generators/reversal.py +66 -0
  87. sofic/generators/stack_hmm.py +426 -0
  88. sofic/generators/stack_inference.py +509 -0
  89. sofic/generators/stationary.py +134 -0
  90. sofic/generators/stochastic.py +65 -0
  91. sofic/generators/synchronization.py +407 -0
  92. sofic/generators/topological_epsilon_enumeration.py +349 -0
  93. sofic/generators/words.py +226 -0
  94. sofic/graph.py +135 -0
  95. sofic/indexing.py +31 -0
  96. sofic/inference/__init__.py +45 -0
  97. sofic/inference/bayesian/__init__.py +68 -0
  98. sofic/inference/bayesian/comparison.py +199 -0
  99. sofic/inference/bayesian/counts.py +219 -0
  100. sofic/inference/bayesian/diversity.py +254 -0
  101. sofic/inference/bayesian/epsilon.py +270 -0
  102. sofic/inference/bayesian/hdp_hmm.py +340 -0
  103. sofic/inference/bayesian/markov.py +294 -0
  104. sofic/inference/bayesian/pymc_backend.py +71 -0
  105. sofic/inference/bayesian/stack_hmm.py +215 -0
  106. sofic/inference/model_selection.py +365 -0
  107. sofic/inference/spectral.py +564 -0
  108. sofic/operations.py +16 -0
  109. sofic/properties.py +339 -0
  110. sofic/serialization.py +450 -0
  111. sofic/shifts/__init__.py +48 -0
  112. sofic/shifts/algorithms.py +84 -0
  113. sofic/shifts/base.py +49 -0
  114. sofic/shifts/cover_construction.py +76 -0
  115. sofic/shifts/covers.py +47 -0
  116. sofic/shifts/dyck_algorithms.py +100 -0
  117. sofic/shifts/dyck_enumeration.py +275 -0
  118. sofic/shifts/markov_dyck.py +172 -0
  119. sofic/shifts/parry_construction.py +82 -0
  120. sofic/shifts/sft.py +104 -0
  121. sofic/shifts/sft_construction.py +52 -0
  122. sofic/shifts/sliding_block_code.py +156 -0
  123. sofic/shifts/sofic.py +111 -0
  124. sofic/shifts/sofic_dyck.py +110 -0
  125. sofic/shifts/sofic_relation.py +64 -0
  126. sofic/shifts/textile.py +104 -0
  127. sofic/shifts/tmc.py +46 -0
  128. sofic/shifts/tmc_construction.py +58 -0
  129. sofic/shifts/topological_anatomy.py +150 -0
  130. sofic/states.py +27 -0
  131. sofic/testing/__init__.py +8 -0
  132. sofic/testing/strategies.py +154 -0
  133. sofic/viz/__init__.py +16 -0
  134. sofic/viz/_context.py +345 -0
  135. sofic/viz/_edge.py +216 -0
  136. sofic/viz/_format.py +89 -0
  137. sofic/viz/_labels.py +34 -0
  138. sofic/viz/_names.py +17 -0
  139. sofic/viz/_rational.py +20 -0
  140. sofic/viz/_tikz_compile.py +177 -0
  141. sofic/viz/_tikz_format.py +122 -0
  142. sofic/viz/_tikz_layout.py +218 -0
  143. sofic/viz/assets/vaucanson.tikz +71 -0
  144. sofic/viz/graphviz.py +158 -0
  145. sofic/viz/idiagram.py +350 -0
  146. sofic/viz/tikz.py +381 -0
  147. sofic-0.1.0.dist-info/METADATA +444 -0
  148. sofic-0.1.0.dist-info/RECORD +150 -0
  149. sofic-0.1.0.dist-info/WHEEL +4 -0
  150. 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)))