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,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
|