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,327 @@
1
+ """Stochastic generator base classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable, Mapping, Sequence
6
+ from typing import TYPE_CHECKING, Any, Self
7
+
8
+ import numpy as np
9
+
10
+ from sofic.base import StateMachine
11
+ from sofic.exceptions import QuasiStochasticValidationError, StochasticValidationError
12
+
13
+ if TYPE_CHECKING:
14
+ from sofic.automata.dfa import DFA
15
+ from sofic.automata.nfa import NFA
16
+ from sofic.generators.mealy import MealyHMM
17
+ from sofic.generators.prob import SymbolConstraints
18
+ from sofic.shifts.sofic import SoficShift
19
+
20
+
21
+ class StochasticModel(StateMachine):
22
+ """Generator with a probability distribution over initial states."""
23
+
24
+ initial_distribution: dict[Hashable, float]
25
+ symbol_constraints: SymbolConstraints | None
26
+
27
+ def __init__(
28
+ self,
29
+ initial_distribution: Mapping[Hashable, float] | None = None,
30
+ *,
31
+ symbol_constraints: SymbolConstraints | None = None,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ super().__init__(**kwargs)
35
+ self.initial_distribution = dict(initial_distribution or {})
36
+ self.symbol_constraints = symbol_constraints
37
+
38
+ def validate(self) -> None:
39
+ self.validate_stochastic()
40
+
41
+ def validate_stochastic(self) -> None:
42
+ from sofic.generators.prob import is_symbolic, row_sums_to_one
43
+
44
+ probs = list(self.initial_distribution.values())
45
+ if probs and not row_sums_to_one(probs):
46
+ total = sum(probs)
47
+ raise StochasticValidationError(f"initial distribution sums to {total}, not 1")
48
+ for state, prob in self.initial_distribution.items():
49
+ if is_symbolic(prob):
50
+ if getattr(prob, "is_negative", None) is True:
51
+ raise StochasticValidationError(f"negative initial probability at {state!r}")
52
+ elif prob < 0:
53
+ raise StochasticValidationError(f"negative initial probability at {state!r}")
54
+ self._require(self.graph.has_state(state), f"unknown initial state {state!r}")
55
+
56
+ def stationary_distribution(self) -> np.ndarray:
57
+ from sofic.generators.stationary import stationary_distribution_hmm
58
+
59
+ return stationary_distribution_hmm(self)
60
+
61
+ def state_distribution(self) -> Any:
62
+ from sofic.generators.measures import state_distribution
63
+
64
+ return state_distribution(self)
65
+
66
+ def state_entropy(self) -> float:
67
+ from sofic.generators.measures import state_entropy
68
+
69
+ return state_entropy(self)
70
+
71
+ def reverse(self) -> Self:
72
+ """Return the time-reversed generator (stochastic reversal, not a graph transpose).
73
+
74
+ Overrides :meth:`sofic.base.StateMachine.reverse` (which merely
75
+ transposes the transition graph): for a stochastic process the reversal
76
+ must reweight edges by the stationary distribution
77
+ (:func:`~sofic.generators.reversal.time_reverse_stochastic`). Emission
78
+ machines are routed through :meth:`EpsilonMachine.from_hmm`.
79
+ """
80
+ from sofic.generators.reversal import is_markov_like, time_reverse_stochastic
81
+
82
+ if not is_markov_like(self):
83
+ from sofic.generators.epsilon_machine import EpsilonMachine
84
+ from sofic.generators.mealy import MealyHMM
85
+ from sofic.generators.moore import MooreHMM
86
+
87
+ if isinstance(self, (MealyHMM, MooreHMM)):
88
+ from sofic.generators.epsilon_machine import EpsilonMachine
89
+
90
+ if isinstance(self, EpsilonMachine):
91
+ return EpsilonMachine.from_time_reversed(self)
92
+ return EpsilonMachine.from_hmm(time_reverse_stochastic(self))
93
+ raise NotImplementedError("time-reversed generators with edge emissions require EpsilonMachine.from_hmm")
94
+ return time_reverse_stochastic(self)
95
+
96
+
97
+ class HiddenMarkovModel(StochasticModel):
98
+ """Hidden-state generator with an observation alphabet."""
99
+
100
+ observation_alphabet: frozenset[Any]
101
+
102
+ def __init__(self, observation_alphabet: frozenset[Any] | None = None, **kwargs: Any) -> None:
103
+ super().__init__(**kwargs)
104
+ self.observation_alphabet = observation_alphabet if observation_alphabet is not None else frozenset()
105
+
106
+ def sample(self, n: int, rng: np.random.Generator | None = None) -> tuple[list[Any], list[Hashable]]:
107
+ from sofic.generators.hmm_inference import sample
108
+
109
+ return sample(self, n, rng)
110
+
111
+ def log_likelihood(self, observations: Sequence[Any]) -> float:
112
+ from sofic.generators.hmm_inference import log_likelihood
113
+
114
+ return log_likelihood(self, observations)
115
+
116
+ def forward(self, observations: Sequence[Any], *, scaled: bool = False) -> np.ndarray:
117
+ from sofic.generators.hmm_inference import forward
118
+
119
+ return forward(self, observations, scaled=scaled)
120
+
121
+ def backward(self, observations: Sequence[Any], *, scaled: bool = False) -> np.ndarray:
122
+ from sofic.generators.hmm_inference import backward
123
+
124
+ return backward(self, observations, scaled=scaled)
125
+
126
+ def viterbi(self, observations: Sequence[Any]) -> list[Hashable]:
127
+ from sofic.generators.hmm_inference import viterbi
128
+
129
+ return viterbi(self, observations)
130
+
131
+ def smooth(self, observations: Sequence[Any]) -> np.ndarray:
132
+ """Return fixed-interval smoothed marginals ``gamma[t, s]``."""
133
+ from sofic.generators.hmm_inference import smooth
134
+
135
+ return smooth(self, observations)
136
+
137
+ def two_slice_marginals(self, observations: Sequence[Any]) -> np.ndarray:
138
+ """Return two-slice smoothed marginals ``xi[t, i, j]``."""
139
+ from sofic.generators.hmm_inference import two_slice_marginals
140
+
141
+ return two_slice_marginals(self, observations)
142
+
143
+ def baum_welch(
144
+ self,
145
+ sequences: Any,
146
+ *,
147
+ max_iter: int = 100,
148
+ tol: float = 1e-6,
149
+ estimate_initial: bool = True,
150
+ ) -> tuple[MealyHMM, list[float]]:
151
+ """Fit parameters by Baum-Welch EM, returning ``(fitted_model, loglik_trace)``."""
152
+ from sofic.generators.hmm_inference import baum_welch
153
+
154
+ return baum_welch(
155
+ self,
156
+ sequences,
157
+ max_iter=max_iter,
158
+ tol=tol,
159
+ estimate_initial=estimate_initial,
160
+ )
161
+
162
+ def score(self, observations: Sequence[Any]) -> dict[tuple[Hashable, Any, Hashable], float]:
163
+ """Return the log-likelihood gradient (Fisher identity) over edge parameters."""
164
+ from sofic.generators.hmm_inference import score
165
+
166
+ return score(self, observations)
167
+
168
+ def observed_information(self, observations: Sequence[Any]) -> np.ndarray:
169
+ """Return the observed information matrix (Louis' identity)."""
170
+ from sofic.generators.hmm_inference import observed_information
171
+
172
+ return observed_information(self, observations)
173
+
174
+ def standard_errors(self, observations: Sequence[Any]) -> dict[tuple[Hashable, Any, Hashable], float]:
175
+ """Return asymptotic standard errors of the free edge parameters."""
176
+ from sofic.generators.hmm_inference import standard_errors
177
+
178
+ return standard_errors(self, observations)
179
+
180
+ def to_mealy(self) -> MealyHMM:
181
+ """Return an equivalent Mealy-style presentation."""
182
+ raise NotImplementedError(f"{type(self).__name__} must implement to_mealy()")
183
+
184
+ def entropy_rate(self) -> float:
185
+ from sofic.generators.measures import entropy_rate_hmm
186
+
187
+ return entropy_rate_hmm(self)
188
+
189
+ def joint_block_distribution(self, history_length: int = 1) -> Any:
190
+ from sofic.generators.measures import joint_block_distribution
191
+
192
+ return joint_block_distribution(self, history_length=history_length)
193
+
194
+ def words_of_length(self, length: int) -> dict[tuple[Any, ...], float]:
195
+ """Return observed words of ``length`` and their probabilities."""
196
+ from sofic.generators.words import hmm_words_of_length
197
+
198
+ return hmm_words_of_length(self, length)
199
+
200
+ def word_probability(
201
+ self,
202
+ word: Sequence[Any],
203
+ *,
204
+ start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None,
205
+ ) -> float:
206
+ """Return the probability of an observed finite word."""
207
+ from sofic.generators.words import hmm_word_probability
208
+
209
+ return hmm_word_probability(self, word, start=start)
210
+
211
+ def log_word_probability(
212
+ self,
213
+ word: Sequence[Any],
214
+ *,
215
+ start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None,
216
+ ) -> float:
217
+ """Return ``log2`` of an observed finite-word probability."""
218
+ from sofic.generators.words import hmm_log_word_probability
219
+
220
+ return hmm_log_word_probability(self, word, start=start)
221
+
222
+ def word_probabilities(
223
+ self,
224
+ lengths: int | Sequence[int],
225
+ *,
226
+ start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None,
227
+ sparse: bool = True,
228
+ ) -> dict[tuple[Any, ...], float]:
229
+ """Return observed-word probabilities for one or more lengths."""
230
+ from sofic.generators.words import hmm_word_probabilities
231
+
232
+ return hmm_word_probabilities(self, lengths, start=start, sparse=sparse)
233
+
234
+ def conditional_word_probability(
235
+ self,
236
+ word: Sequence[Any],
237
+ condition: Sequence[Any],
238
+ *,
239
+ start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None,
240
+ ) -> float:
241
+ """Return ``P(word | condition)``."""
242
+ from sofic.generators.words import hmm_conditional_word_probability
243
+
244
+ return hmm_conditional_word_probability(self, word, condition, start=start)
245
+
246
+ def is_equal_process(
247
+ self,
248
+ other: HiddenMarkovModel,
249
+ *,
250
+ start1: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None,
251
+ start2: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None,
252
+ rtol: float | None = None,
253
+ atol: float | None = None,
254
+ ) -> bool:
255
+ """Return whether two HMMs generate the same finite-word process."""
256
+ from sofic.generators.process_equivalence import is_equal_process
257
+
258
+ return is_equal_process(self, other, start1=start1, start2=start2, rtol=rtol, atol=atol)
259
+
260
+ def to_sofic_shift(self) -> SoficShift:
261
+ """Strip probabilities and return a sofic shift with the same support."""
262
+ from sofic.generators.conversions import hmm_to_sofic_shift
263
+
264
+ return hmm_to_sofic_shift(self)
265
+
266
+ def to_support_nfa(self) -> NFA:
267
+ """Return an NFA for the support language of this HMM."""
268
+ from sofic.generators.conversions import hmm_to_support_nfa
269
+
270
+ return hmm_to_support_nfa(self)
271
+
272
+ def to_support_dfa(self) -> DFA:
273
+ """Return a determinized automaton for the support language of this HMM."""
274
+ from sofic.generators.conversions import hmm_to_support_dfa
275
+
276
+ return hmm_to_support_dfa(self)
277
+
278
+
279
+ class QuasiStochasticModel(StateMachine):
280
+ """Generator allowing signed quasiprobabilities on internal weights."""
281
+
282
+ initial_quasidistribution: dict[Hashable, float]
283
+
284
+ def __init__(self, initial_quasidistribution: Mapping[Hashable, float] | None = None, **kwargs: Any) -> None:
285
+ super().__init__(**kwargs)
286
+ self.initial_quasidistribution = dict(initial_quasidistribution or {})
287
+
288
+ def validate(self) -> None:
289
+ self.validate_quasistochastic()
290
+
291
+ def validate_quasistochastic(self) -> None:
292
+ total = sum(self.initial_quasidistribution.values())
293
+ if not np.isclose(total, 1.0):
294
+ raise QuasiStochasticValidationError(f"initial quasidistribution sums to {total}, not 1")
295
+ for state in self.initial_quasidistribution:
296
+ self._require(self.graph.has_state(state), f"unknown initial state {state!r}")
297
+
298
+ def word_probability(self, word: Sequence[Any]) -> float:
299
+ from sofic.generators.quasi_inference import word_probability
300
+
301
+ return word_probability(self, word)
302
+
303
+ def stationary_quasidistribution(self) -> np.ndarray:
304
+ from sofic.generators.quasi_inference import stationary_quasidistribution
305
+
306
+ return stationary_quasidistribution(self)
307
+
308
+ def transition_matrices(self) -> dict[Any, np.ndarray]:
309
+ from sofic.generators.quasi_inference import transition_matrices
310
+
311
+ return transition_matrices(self)
312
+
313
+ def words_of_length(self, length: int) -> dict[tuple[Any, ...], float]:
314
+ """Return words of ``length`` and their signed quasiprobabilities."""
315
+ from sofic.generators.words import quasi_words_of_length
316
+
317
+ return quasi_words_of_length(self, length)
318
+
319
+ def collision_entropy(self) -> float:
320
+ from sofic.generators.measures import collision_entropy
321
+
322
+ return collision_entropy(self)
323
+
324
+ def process_negativity(self) -> float:
325
+ from sofic.generators.measures import process_negativity
326
+
327
+ return process_negativity(self)