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
sofic/properties.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Structural predicates for sofic state-machine models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import networkx as nx
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from sofic.base import StateMachine
|
|
12
|
+
from sofic.graph import ATTR_EMISSION, ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL, EPSILON
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_unifilar_labeled(
|
|
16
|
+
model: StateMachine,
|
|
17
|
+
*,
|
|
18
|
+
label_attr: str,
|
|
19
|
+
exclude_labels: Iterable[Any] = (),
|
|
20
|
+
) -> bool:
|
|
21
|
+
"""Return whether each state has at most one outgoing edge per label value."""
|
|
22
|
+
excluded = set(exclude_labels)
|
|
23
|
+
seen: set[tuple[Hashable, Any]] = set()
|
|
24
|
+
for transition in model.transitions():
|
|
25
|
+
label = transition.data.get(label_attr)
|
|
26
|
+
if label is None or label in excluded:
|
|
27
|
+
continue
|
|
28
|
+
key = (transition.source, label)
|
|
29
|
+
if key in seen:
|
|
30
|
+
return False
|
|
31
|
+
seen.add(key)
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_unifilar_symbols(model: StateMachine) -> bool:
|
|
36
|
+
"""Right-resolving on input symbols (ε excluded)."""
|
|
37
|
+
return is_unifilar_labeled(model, label_attr=ATTR_SYMBOL, exclude_labels=(EPSILON,))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_unifilar_emissions(model: StateMachine) -> bool:
|
|
41
|
+
"""Row-unifilar on edge emissions (CM generator sense)."""
|
|
42
|
+
return is_unifilar_labeled(model, label_attr=ATTR_EMISSION)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_counifilar_emissions(model: StateMachine) -> bool:
|
|
46
|
+
"""Column-unifilar on edge emissions: ``(target, emission)`` identifies the source."""
|
|
47
|
+
seen: dict[tuple[Hashable, Any], Hashable] = {}
|
|
48
|
+
for transition in model.transitions():
|
|
49
|
+
emission = transition.data.get(ATTR_EMISSION)
|
|
50
|
+
if emission is None:
|
|
51
|
+
continue
|
|
52
|
+
key = (transition.target, emission)
|
|
53
|
+
source = seen.get(key)
|
|
54
|
+
if source is not None and source != transition.source:
|
|
55
|
+
return False
|
|
56
|
+
seen[key] = transition.source
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_deterministic_automaton(aut: Any) -> bool:
|
|
61
|
+
"""DFA-style determinism: one initial, no ε, unifilar on symbols."""
|
|
62
|
+
if len(aut.initial_states) != 1:
|
|
63
|
+
return False
|
|
64
|
+
for transition in aut.transitions():
|
|
65
|
+
if transition.data.get(ATTR_SYMBOL) is EPSILON:
|
|
66
|
+
return False
|
|
67
|
+
return is_unifilar_symbols(aut)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_deterministic_markov(chain: StateMachine) -> bool:
|
|
71
|
+
"""Each state has exactly one successor with probability 1."""
|
|
72
|
+
for state in chain.states():
|
|
73
|
+
outgoing = list(chain.graph.out_transitions(state))
|
|
74
|
+
if len(outgoing) != 1:
|
|
75
|
+
return False
|
|
76
|
+
prob = float(outgoing[0].data.get(ATTR_PROB, 0.0))
|
|
77
|
+
if not np.isclose(prob, 1.0):
|
|
78
|
+
return False
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_deterministic_transducer(tr: StateMachine) -> bool:
|
|
83
|
+
"""At most one transition per (state, input symbol)."""
|
|
84
|
+
return is_unifilar_symbols(tr)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_unifilar_transducer(tr: StateMachine) -> bool:
|
|
88
|
+
"""Causal (joint) unifilarity: each ``(state, input, output)`` has at most one successor.
|
|
89
|
+
|
|
90
|
+
This is the transducer analog of :func:`is_unifilar_emissions`: appending an
|
|
91
|
+
observed input-output pair ``(x, y)`` determines the next causal state, as in
|
|
92
|
+
the ε-transducer of Barnett & Crutchfield (2015).
|
|
93
|
+
"""
|
|
94
|
+
seen: set[tuple[Hashable, Any, Any]] = set()
|
|
95
|
+
for transition in tr.transitions():
|
|
96
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
97
|
+
if symbol is EPSILON:
|
|
98
|
+
continue
|
|
99
|
+
key = (transition.source, symbol, transition.data.get(ATTR_OUTPUT))
|
|
100
|
+
if key in seen:
|
|
101
|
+
return False
|
|
102
|
+
seen.add(key)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_counifilar_transducer(tr: StateMachine) -> bool:
|
|
107
|
+
"""Co-unifilar: each ``(target, input, output)`` identifies a unique source."""
|
|
108
|
+
seen: dict[tuple[Hashable, Any, Any], Hashable] = {}
|
|
109
|
+
for transition in tr.transitions():
|
|
110
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
111
|
+
if symbol is EPSILON:
|
|
112
|
+
continue
|
|
113
|
+
key = (transition.target, symbol, transition.data.get(ATTR_OUTPUT))
|
|
114
|
+
source = seen.get(key)
|
|
115
|
+
if source is not None and source != transition.source:
|
|
116
|
+
return False
|
|
117
|
+
seen[key] = transition.source
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_sequential_transducer(tr: Any) -> bool:
|
|
122
|
+
"""Input-deterministic (sequential) transducer: single initial state, no ε
|
|
123
|
+
input, and at most one outgoing edge per ``(state, input)``."""
|
|
124
|
+
initial = getattr(tr, "initial_states", frozenset())
|
|
125
|
+
if len(initial) > 1:
|
|
126
|
+
return False
|
|
127
|
+
seen: set[tuple[Hashable, Any]] = set()
|
|
128
|
+
for transition in tr.transitions():
|
|
129
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
130
|
+
if symbol is EPSILON:
|
|
131
|
+
return False
|
|
132
|
+
key = (transition.source, symbol)
|
|
133
|
+
if key in seen:
|
|
134
|
+
return False
|
|
135
|
+
seen.add(key)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_subsequential_transducer(tr: Any) -> bool:
|
|
140
|
+
"""Sequential transducer equipped with a per-state final-output function."""
|
|
141
|
+
return is_sequential_transducer(tr) and getattr(tr, "final_output", None) is not None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_irreducible(model: StateMachine) -> bool:
|
|
145
|
+
"""Return whether the directed state graph is strongly connected."""
|
|
146
|
+
graph = _simple_digraph(model)
|
|
147
|
+
if graph.number_of_nodes() <= 1:
|
|
148
|
+
return True
|
|
149
|
+
return bool(nx.is_strongly_connected(graph))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def recurrent_components(model: StateMachine) -> list[frozenset[Hashable]]:
|
|
153
|
+
"""Terminal strongly connected components of the directed state graph."""
|
|
154
|
+
graph = _simple_digraph(model)
|
|
155
|
+
components: list[frozenset[Hashable]] = []
|
|
156
|
+
for component in nx.strongly_connected_components(graph):
|
|
157
|
+
exits = any(target not in component for source in component for target in graph.successors(source))
|
|
158
|
+
if not exits:
|
|
159
|
+
components.append(frozenset(component))
|
|
160
|
+
return components
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def recurrent_states(model: StateMachine) -> frozenset[Hashable]:
|
|
164
|
+
"""States in terminal strongly connected components."""
|
|
165
|
+
states: set[Hashable] = set()
|
|
166
|
+
for component in recurrent_components(model):
|
|
167
|
+
states.update(component)
|
|
168
|
+
return frozenset(states)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def is_ergodic(model: StateMachine, *, weak: bool = True) -> bool:
|
|
172
|
+
"""Return weak/strong ergodicity of the internal finite-state dynamics.
|
|
173
|
+
|
|
174
|
+
Weak ergodicity means all positive-mass initial states can reach exactly
|
|
175
|
+
one terminal SCC. Strong ergodicity additionally requires that terminal
|
|
176
|
+
component to be aperiodic.
|
|
177
|
+
"""
|
|
178
|
+
graph = _simple_digraph(model)
|
|
179
|
+
components = recurrent_components(model)
|
|
180
|
+
if not components:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
initial = getattr(model, "initial_distribution", {})
|
|
184
|
+
starts = {state for state, mass in initial.items() if mass > 0.0} if initial else set(model.states())
|
|
185
|
+
|
|
186
|
+
reachable_terminal: list[frozenset[Hashable]] = []
|
|
187
|
+
for component in components:
|
|
188
|
+
if any(
|
|
189
|
+
source in graph and target in nx.descendants(graph, source) | {source}
|
|
190
|
+
for source in starts
|
|
191
|
+
for target in component
|
|
192
|
+
):
|
|
193
|
+
reachable_terminal.append(component)
|
|
194
|
+
|
|
195
|
+
if len(reachable_terminal) != 1:
|
|
196
|
+
return False
|
|
197
|
+
if weak:
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
subgraph = graph.subgraph(reachable_terminal[0]).copy()
|
|
201
|
+
return bool(nx.is_aperiodic(subgraph))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def is_stationary(model: StateMachine, *, rtol: float = 1e-8, atol: float = 1e-10) -> bool:
|
|
205
|
+
"""Return whether ``initial_distribution`` is invariant under internal dynamics."""
|
|
206
|
+
initial_distribution = getattr(model, "initial_distribution", None)
|
|
207
|
+
if initial_distribution is None:
|
|
208
|
+
return False
|
|
209
|
+
vector, transition = _initial_vector_and_transition(model)
|
|
210
|
+
return bool(np.allclose(vector @ transition, vector, rtol=rtol, atol=atol))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def is_detailed_balance(model: StateMachine, *, rtol: float = 1e-8, atol: float = 1e-10) -> bool:
|
|
214
|
+
"""Return whether stationary labeled flows satisfy detailed balance."""
|
|
215
|
+
try:
|
|
216
|
+
pi = np.asarray(model.stationary_distribution(), dtype=float)
|
|
217
|
+
except Exception:
|
|
218
|
+
pi, _transition = _initial_vector_and_transition(model)
|
|
219
|
+
|
|
220
|
+
matrices = _labeled_or_internal_matrices(model)
|
|
221
|
+
for matrix in matrices:
|
|
222
|
+
flow = pi[:, None] * matrix
|
|
223
|
+
if not np.allclose(flow, flow.T, rtol=rtol, atol=atol):
|
|
224
|
+
return False
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_periodic(model: StateMachine) -> bool:
|
|
229
|
+
"""Return whether every terminal component has graph period greater than one."""
|
|
230
|
+
graph = _simple_digraph(model)
|
|
231
|
+
components = recurrent_components(model)
|
|
232
|
+
if not components:
|
|
233
|
+
return False
|
|
234
|
+
periodic_components = 0
|
|
235
|
+
for component in components:
|
|
236
|
+
subgraph = graph.subgraph(component).copy()
|
|
237
|
+
if subgraph.number_of_nodes() == 0:
|
|
238
|
+
continue
|
|
239
|
+
if nx.is_aperiodic(subgraph):
|
|
240
|
+
return False
|
|
241
|
+
periodic_components += 1
|
|
242
|
+
return periodic_components > 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def is_strictly_sofic(model: Any) -> bool:
|
|
246
|
+
"""Return whether the HMM support appears strictly sofic.
|
|
247
|
+
|
|
248
|
+
For finite right-resolving support presentations, finite Markov order is
|
|
249
|
+
the finite-type case; infinite Markov order is strictly sofic.
|
|
250
|
+
"""
|
|
251
|
+
from sofic.generators.conversions import hmm_to_support_dfa
|
|
252
|
+
from sofic.generators.synchronization import (
|
|
253
|
+
build_topological_graph_from_transitions,
|
|
254
|
+
graph_from_unifilar_automaton,
|
|
255
|
+
markov_order_from_graph,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
support = model.to_sofic_shift()
|
|
259
|
+
if support.is_unifilar():
|
|
260
|
+
transitions: dict[tuple[Hashable, Any], Hashable] = {}
|
|
261
|
+
alphabet = set(support.symbol_alphabet)
|
|
262
|
+
for transition in support.transitions():
|
|
263
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
264
|
+
if symbol is None:
|
|
265
|
+
continue
|
|
266
|
+
transitions[(transition.source, symbol)] = transition.target
|
|
267
|
+
alphabet.add(symbol)
|
|
268
|
+
graph = build_topological_graph_from_transitions(
|
|
269
|
+
states=frozenset(support.states()),
|
|
270
|
+
alphabet=frozenset(alphabet),
|
|
271
|
+
transitions=transitions,
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
graph = graph_from_unifilar_automaton(hmm_to_support_dfa(model))
|
|
275
|
+
return markov_order_from_graph(graph) == float("inf")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _simple_digraph(model: StateMachine) -> nx.DiGraph:
|
|
279
|
+
graph = nx.DiGraph()
|
|
280
|
+
graph.add_nodes_from(model.states())
|
|
281
|
+
graph.add_edges_from((transition.source, transition.target) for transition in model.transitions())
|
|
282
|
+
return graph
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def transition_matrix(
|
|
286
|
+
model: StateMachine,
|
|
287
|
+
*,
|
|
288
|
+
attr: str = ATTR_PROB,
|
|
289
|
+
states: Iterable[Hashable] | None = None,
|
|
290
|
+
) -> tuple[np.ndarray, list[Hashable]]:
|
|
291
|
+
"""Return the dense state-to-state matrix accumulating edge ``attr`` weights.
|
|
292
|
+
|
|
293
|
+
Rows/columns follow ``states`` when given (edges to states outside the set
|
|
294
|
+
are ignored), otherwise all model states in iteration order. Returns the
|
|
295
|
+
matrix together with the ordered state list defining its axes.
|
|
296
|
+
"""
|
|
297
|
+
ordered = list(states) if states is not None else list(model.states())
|
|
298
|
+
index = {state: i for i, state in enumerate(ordered)}
|
|
299
|
+
n = len(ordered)
|
|
300
|
+
from sofic.generators.prob import as_prob, has_symbolic, zeros
|
|
301
|
+
|
|
302
|
+
edge_probs = []
|
|
303
|
+
for state in ordered:
|
|
304
|
+
for transition in model.graph.out_transitions(state):
|
|
305
|
+
if transition.target in index:
|
|
306
|
+
edge_probs.append(transition.data.get(attr, 0.0))
|
|
307
|
+
symbolic = has_symbolic(edge_probs)
|
|
308
|
+
matrix = zeros((n, n), symbolic=symbolic)
|
|
309
|
+
for state in ordered:
|
|
310
|
+
i = index[state]
|
|
311
|
+
for transition in model.graph.out_transitions(state):
|
|
312
|
+
j = index.get(transition.target)
|
|
313
|
+
if j is None:
|
|
314
|
+
continue
|
|
315
|
+
matrix[i, j] = as_prob(matrix[i, j]) + as_prob(transition.data.get(attr, 0.0))
|
|
316
|
+
return matrix, ordered
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _initial_vector_and_transition(model: StateMachine) -> tuple[np.ndarray, np.ndarray]:
|
|
320
|
+
idx = model.reindex()
|
|
321
|
+
n = len(idx)
|
|
322
|
+
vector = np.zeros(n, dtype=float)
|
|
323
|
+
for state, mass in getattr(model, "initial_distribution", {}).items():
|
|
324
|
+
vector[idx.index(state)] = float(mass)
|
|
325
|
+
transition, _states = transition_matrix(model, attr=ATTR_PROB, states=idx.states)
|
|
326
|
+
return vector, transition
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _labeled_or_internal_matrices(model: StateMachine) -> list[np.ndarray]:
|
|
330
|
+
observation_alphabet = getattr(model, "observation_alphabet", None)
|
|
331
|
+
to_mealy = getattr(model, "to_mealy", None)
|
|
332
|
+
if observation_alphabet is not None and to_mealy is not None:
|
|
333
|
+
from sofic.generators.hmm_inference import _emission_transition_tensors_from_mealy
|
|
334
|
+
|
|
335
|
+
_pi, matrices = _emission_transition_tensors_from_mealy(to_mealy())
|
|
336
|
+
return list(matrices.values())
|
|
337
|
+
|
|
338
|
+
_initial, transition = _initial_vector_and_transition(model)
|
|
339
|
+
return [transition]
|