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,193 @@
1
+ """Epsilon machine construction via causal-state merging.
2
+
3
+ For a unifilar finite predictive model, merge probabilistically equivalent
4
+ states by Hopcroft-style partition refinement on ``(symbol, probability,
5
+ successor_block)`` labels (Loomis & Crutchfield, arXiv:1808.08639, Cor. 1).
6
+ Non-unifilar inputs are first converted to a unifilar mixed-state presentation
7
+ (Ellison, Mahoney & Crutchfield, J. Stat. Phys. 2009).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import defaultdict
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+
17
+ from sofic.exceptions import StochasticValidationError
18
+ from sofic.generators.base import HiddenMarkovModel
19
+ from sofic.generators.epsilon_machine import EpsilonMachine
20
+ from sofic.generators.mealy import MealyHMM
21
+ from sofic.generators.mixed_state import MixedStatePresentation
22
+ from sofic.generators.prob import (
23
+ array_sum,
24
+ as_prob,
25
+ canonical_prob_key,
26
+ has_symbolic,
27
+ is_positive_mass,
28
+ is_zero,
29
+ simplify_prob,
30
+ sum_probs,
31
+ )
32
+ from sofic.graph import ATTR_EMISSION, ATTR_PROB, TransitionGraph
33
+ from sofic.states import sequential_labels
34
+
35
+ TransitionSignature = tuple[tuple[Any, int, Any], ...]
36
+
37
+
38
+ def build_epsilon_machine(hmm: HiddenMarkovModel) -> EpsilonMachine:
39
+ hmm = hmm.to_mealy()
40
+ presentation = _unifilar_presentation(hmm)
41
+ stationary = presentation.stationary_distribution()
42
+ if is_zero(array_sum(stationary)):
43
+ raise StochasticValidationError("generator must have a stationary distribution")
44
+
45
+ constraints = getattr(presentation, "symbol_constraints", None) or getattr(hmm, "symbol_constraints", None)
46
+ partitions = _refine_probabilistic_partitions(presentation, constraints=constraints)
47
+ return _quotient_machine(presentation, partitions, stationary, constraints=constraints)
48
+
49
+
50
+ def _unifilar_presentation(hmm: MealyHMM) -> MealyHMM:
51
+ """Return a row-unifilar presentation generating the same process."""
52
+ if not hmm.is_unifilar():
53
+ hmm = hmm.mixed_state_presentation()
54
+ if isinstance(hmm, MixedStatePresentation) and hmm.recurrent_states:
55
+ return hmm.to_recurrent()
56
+ return hmm
57
+
58
+
59
+ def _refine_probabilistic_partitions(
60
+ hmm: MealyHMM,
61
+ *,
62
+ constraints: Any = None,
63
+ ) -> list[set[Any]]:
64
+ """Hopcroft-style refinement on probabilistic transition signatures."""
65
+ partitions: list[set[Any]] = [set(hmm.states())]
66
+ changed = True
67
+ while changed:
68
+ changed = False
69
+ state_to_block = _state_to_block_index(partitions)
70
+ new_partitions: list[set[Any]] = []
71
+ for block in partitions:
72
+ subblocks = _split_block(hmm, block, state_to_block, constraints=constraints)
73
+ if len(subblocks) > 1:
74
+ changed = True
75
+ new_partitions.extend(subblocks)
76
+ partitions = new_partitions
77
+ return partitions
78
+
79
+
80
+ def _state_to_block_index(partitions: list[set[Any]]) -> dict[Any, int]:
81
+ mapping: dict[Any, int] = {}
82
+ for index, block in enumerate(partitions):
83
+ for state in block:
84
+ mapping[state] = index
85
+ return mapping
86
+
87
+
88
+ def _split_block(
89
+ hmm: MealyHMM,
90
+ block: set[Any],
91
+ state_to_block: dict[Any, int],
92
+ *,
93
+ constraints: Any = None,
94
+ ) -> list[set[Any]]:
95
+ """Split a block when states disagree on labeled successor blocks."""
96
+ futures: dict[TransitionSignature, set[Any]] = defaultdict(set)
97
+ for state in block:
98
+ futures[_transition_signature(hmm, state, state_to_block, constraints=constraints)].add(state)
99
+ return list(futures.values())
100
+
101
+
102
+ def _transition_signature(
103
+ hmm: MealyHMM,
104
+ state: Any,
105
+ state_to_block: dict[Any, int],
106
+ *,
107
+ constraints: Any = None,
108
+ ) -> TransitionSignature:
109
+ triples: list[tuple[Any, int, Any]] = []
110
+ for transition in hmm.graph.out_transitions(state):
111
+ emission = transition.data.get(ATTR_EMISSION)
112
+ if emission is None:
113
+ continue
114
+ prob = as_prob(transition.data.get(ATTR_PROB, 0.0))
115
+ triples.append(
116
+ (
117
+ emission,
118
+ state_to_block[transition.target],
119
+ canonical_prob_key(prob, constraints),
120
+ )
121
+ )
122
+ return tuple(sorted(triples, key=repr))
123
+
124
+
125
+ def _quotient_machine(
126
+ hmm: MealyHMM,
127
+ partitions: list[set[Any]],
128
+ stationary: np.ndarray,
129
+ *,
130
+ constraints: Any = None,
131
+ ) -> EpsilonMachine:
132
+ state_map: dict[Any, int] = {}
133
+ for index, block in enumerate(partitions):
134
+ for state in block:
135
+ state_map[state] = index
136
+
137
+ graph = TransitionGraph()
138
+ labels = sequential_labels(len(partitions))
139
+ for label in labels:
140
+ graph.add_state(label)
141
+ label_for_index = {index: labels[index] for index in range(len(partitions))}
142
+
143
+ for block_index, block in enumerate(partitions):
144
+ representative = next(iter(block))
145
+ source = label_for_index[block_index]
146
+ for transition in hmm.graph.out_transitions(representative):
147
+ emission = transition.data.get(ATTR_EMISSION)
148
+ if emission is None:
149
+ continue
150
+ target = label_for_index[state_map[transition.target]]
151
+ prob = as_prob(transition.data.get(ATTR_PROB, 0.0))
152
+ graph.add_transition(
153
+ source,
154
+ target,
155
+ **{ATTR_PROB: prob, ATTR_EMISSION: emission},
156
+ )
157
+
158
+ symbolic = stationary.dtype == object or has_symbolic(stationary.ravel())
159
+ if symbolic:
160
+ import sympy as sp
161
+
162
+ initial = [sp.Integer(0)] * len(partitions)
163
+ for state, mass in hmm.initial_distribution.items():
164
+ initial[state_map[state]] = simplify_prob(as_prob(initial[state_map[state]]) + as_prob(mass))
165
+ if is_zero(sum_probs(initial)):
166
+ idx = hmm.reindex()
167
+ for state, mass in zip(idx.states, stationary, strict=False):
168
+ initial[state_map[state]] = simplify_prob(as_prob(initial[state_map[state]]) + as_prob(mass))
169
+ total = sum_probs(initial)
170
+ initial_dist = {
171
+ label_for_index[i]: simplify_prob(as_prob(initial[i]) / total)
172
+ for i in range(len(partitions))
173
+ if is_positive_mass(initial[i])
174
+ }
175
+ else:
176
+ initial = np.zeros(len(partitions), dtype=float)
177
+ for state, mass in hmm.initial_distribution.items():
178
+ initial[state_map[state]] += float(mass)
179
+ if initial.sum() <= 0.0:
180
+ idx = hmm.reindex()
181
+ for state, mass in zip(idx.states, stationary, strict=False):
182
+ initial[state_map[state]] += float(mass)
183
+ initial /= initial.sum()
184
+ initial_dist = {label_for_index[i]: float(initial[i]) for i in range(len(partitions)) if initial[i] > 0.0}
185
+
186
+ eps = EpsilonMachine(
187
+ graph=graph,
188
+ initial_distribution=initial_dist,
189
+ observation_alphabet=hmm.observation_alphabet,
190
+ symbol_constraints=constraints,
191
+ )
192
+ eps.validate()
193
+ return eps