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,168 @@
|
|
|
1
|
+
"""Epsilon-transducers: minimal unifilar presentations of channels.
|
|
2
|
+
|
|
3
|
+
Computational mechanics quantifies structure in a single stochastic process via
|
|
4
|
+
its causal states, yielding the minimal optimal predictor -- the ε-machine.
|
|
5
|
+
Barnett & Crutchfield (*Computational Mechanics of Input-Output Processes:
|
|
6
|
+
Structured Transformations and the ε-Transducer*, J. Stat. Phys. 161:2 (2015)
|
|
7
|
+
404-451, doi:10.1007/s10955-015-1327-5) extend this to communication channels
|
|
8
|
+
coupling two processes, obtaining the ε-transducer: the tuple
|
|
9
|
+
``(X, Y, S, T)`` of input alphabet, output alphabet, causal states, and
|
|
10
|
+
conditional-symbol transition probabilities ``T(y, s' | s, x)``.
|
|
11
|
+
|
|
12
|
+
Structurally the ε-transducer is a unifilar, input-driven stochastic Mealy
|
|
13
|
+
machine, so :class:`EpsilonTransducer` extends the probability-aware
|
|
14
|
+
:class:`~sofic.automata.transducers.MealyMachine` with a causal-state initial
|
|
15
|
+
distribution, causal-state minimization, transCSSR reconstruction, and channel
|
|
16
|
+
information measures.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Hashable, Mapping, Sequence
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from sofic.automata.transducers import MealyMachine
|
|
27
|
+
from sofic.exceptions import StochasticValidationError, UnifilarityError
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sofic.generators.base import HiddenMarkovModel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EpsilonTransducer(MealyMachine):
|
|
34
|
+
"""Unifilar stochastic Mealy machine: minimal causal presentation of a channel.
|
|
35
|
+
|
|
36
|
+
The channel law lives on edges as ``T(y, s' | s, x)``: each outgoing edge of
|
|
37
|
+
causal state ``s`` carries an input symbol ``x``, an output symbol ``y``, and
|
|
38
|
+
a probability, with the probabilities of all ``(y, s')`` sharing a given
|
|
39
|
+
``(s, x)`` summing to one. Unifilarity means the observed pair ``(x, y)``
|
|
40
|
+
determines the successor causal state.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
>>> from sofic.examples.processes import BinaryChannel
|
|
45
|
+
>>> from sofic import EpsilonTransducer
|
|
46
|
+
>>> channel = BinaryChannel(0.1, 0.2)
|
|
47
|
+
>>> eps = EpsilonTransducer.from_channel(channel)
|
|
48
|
+
>>> eps.is_unifilar()
|
|
49
|
+
True
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
initial_distribution: dict[Hashable, float]
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
input_alphabet: frozenset[Any] | None = None,
|
|
57
|
+
output_alphabet: frozenset[Any] | None = None,
|
|
58
|
+
initial_states: frozenset[Hashable] | None = None,
|
|
59
|
+
initial_distribution: Mapping[Hashable, float] | None = None,
|
|
60
|
+
**kwargs: Any,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(
|
|
63
|
+
input_alphabet=input_alphabet,
|
|
64
|
+
output_alphabet=output_alphabet,
|
|
65
|
+
initial_states=initial_states,
|
|
66
|
+
**kwargs,
|
|
67
|
+
)
|
|
68
|
+
self.initial_distribution = dict(initial_distribution or {})
|
|
69
|
+
|
|
70
|
+
def validate(self) -> None:
|
|
71
|
+
super().validate()
|
|
72
|
+
self.validate_stochastic()
|
|
73
|
+
self._check_unifilar()
|
|
74
|
+
self._validate_initial_distribution()
|
|
75
|
+
|
|
76
|
+
def _validate_initial_distribution(self) -> None:
|
|
77
|
+
if not self.initial_distribution:
|
|
78
|
+
return
|
|
79
|
+
total = float(sum(self.initial_distribution.values()))
|
|
80
|
+
if not np.isclose(total, 1.0):
|
|
81
|
+
raise StochasticValidationError(f"initial distribution sums to {total}, not 1")
|
|
82
|
+
for state, mass in self.initial_distribution.items():
|
|
83
|
+
if mass < 0:
|
|
84
|
+
raise StochasticValidationError(f"negative initial probability at {state!r}")
|
|
85
|
+
self._require(self.graph.has_state(state), f"unknown initial state {state!r}")
|
|
86
|
+
|
|
87
|
+
def _check_unifilar(self) -> None:
|
|
88
|
+
if self.is_unifilar():
|
|
89
|
+
return
|
|
90
|
+
raise UnifilarityError("epsilon-transducer must be unifilar on (state, input, output)")
|
|
91
|
+
|
|
92
|
+
def is_unifilar(self) -> bool:
|
|
93
|
+
"""Return whether ``(state, input, output)`` determines the successor state."""
|
|
94
|
+
from sofic.properties import is_unifilar_transducer
|
|
95
|
+
|
|
96
|
+
return is_unifilar_transducer(self)
|
|
97
|
+
|
|
98
|
+
def causal_states(self) -> list[Hashable]:
|
|
99
|
+
"""Return the causal states (an alias for :meth:`states`)."""
|
|
100
|
+
return list(self.states())
|
|
101
|
+
|
|
102
|
+
# -- construction ---------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_channel(cls, channel: MealyMachine, **kwargs: Any) -> EpsilonTransducer:
|
|
106
|
+
"""Minimize a (joint-unifilar) stochastic transducer to causal states."""
|
|
107
|
+
from sofic.generators.epsilon_transducer_construction import build_epsilon_transducer
|
|
108
|
+
|
|
109
|
+
return build_epsilon_transducer(channel, **kwargs)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_iohmm(cls, channel: MealyMachine, **kwargs: Any) -> EpsilonTransducer:
|
|
113
|
+
"""Alias for :meth:`from_channel` (an input-output HMM is a stochastic Mealy machine)."""
|
|
114
|
+
return cls.from_channel(channel, **kwargs)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_joint_generator(cls, generator: HiddenMarkovModel, **kwargs: Any) -> EpsilonTransducer:
|
|
118
|
+
"""Build the ε-transducer from a generator emitting ``(input, output)`` pairs."""
|
|
119
|
+
from sofic.generators.epsilon_transducer_construction import from_joint_generator
|
|
120
|
+
|
|
121
|
+
return from_joint_generator(generator, **kwargs)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_paired_sequences(
|
|
125
|
+
cls,
|
|
126
|
+
inputs: Sequence[Any],
|
|
127
|
+
outputs: Sequence[Any],
|
|
128
|
+
**kwargs: Any,
|
|
129
|
+
) -> EpsilonTransducer:
|
|
130
|
+
"""Reconstruct an ε-transducer from paired input/output sequences via transCSSR."""
|
|
131
|
+
from sofic.generators.epsilon_transducer_inference import transcssr
|
|
132
|
+
|
|
133
|
+
return transcssr(inputs, outputs, **kwargs)
|
|
134
|
+
|
|
135
|
+
# -- channel measures -----------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def statistical_complexity(self, input_process: HiddenMarkovModel) -> float:
|
|
138
|
+
"""Channel statistical complexity ``H[S]`` when driven by ``input_process``."""
|
|
139
|
+
from sofic.generators.channel_measures import channel_statistical_complexity
|
|
140
|
+
|
|
141
|
+
return channel_statistical_complexity(self, input_process)
|
|
142
|
+
|
|
143
|
+
def driven_entropy_rate(self, input_process: HiddenMarkovModel) -> float:
|
|
144
|
+
"""Entropy rate of the output process induced by ``input_process``."""
|
|
145
|
+
from sofic.generators.channel_measures import driven_entropy_rate
|
|
146
|
+
|
|
147
|
+
return driven_entropy_rate(self, input_process)
|
|
148
|
+
|
|
149
|
+
def directed_information(self, input_process: HiddenMarkovModel, *, length: int = 1) -> float:
|
|
150
|
+
"""Directed information ``I(X^n -> Y^n)`` when driven by ``input_process``."""
|
|
151
|
+
from sofic.generators.channel_measures import directed_information
|
|
152
|
+
|
|
153
|
+
return directed_information(self, input_process, length=length)
|
|
154
|
+
|
|
155
|
+
def transfer_entropy(self, input_process: HiddenMarkovModel, *, history: int = 1) -> float:
|
|
156
|
+
"""Transfer entropy from input to output when driven by ``input_process``."""
|
|
157
|
+
from sofic.generators.channel_measures import transfer_entropy
|
|
158
|
+
|
|
159
|
+
return transfer_entropy(self, input_process, history=history)
|
|
160
|
+
|
|
161
|
+
# -- conversions ----------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_wfst(cls, wfst: Any) -> EpsilonTransducer:
|
|
165
|
+
"""Build an ε-transducer from a probability-semiring weighted transducer."""
|
|
166
|
+
if getattr(wfst, "semiring", "probability") != "probability":
|
|
167
|
+
raise ValueError("from_wfst requires a probability-semiring WFST")
|
|
168
|
+
return cls.from_channel(MealyMachine.from_networkx(wfst.to_networkx(), initial_states=wfst.initial_states))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Epsilon-transducer construction via causal-state merging.
|
|
2
|
+
|
|
3
|
+
For a joint-unifilar stochastic transducer, merge channel-equivalent states by
|
|
4
|
+
Hopcroft-style partition refinement on ``(input, output, probability,
|
|
5
|
+
successor_block)`` transition signatures -- the input-output analog of the
|
|
6
|
+
ε-machine construction in :mod:`sofic.generators.epsilon_construction` (Barnett &
|
|
7
|
+
Crutchfield, J. Stat. Phys. 161:2 (2015)).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from sofic.automata.transducers import MealyMachine
|
|
16
|
+
from sofic.exceptions import StochasticValidationError
|
|
17
|
+
from sofic.generators.epsilon_transducer import EpsilonTransducer
|
|
18
|
+
from sofic.graph import ATTR_EMISSION, ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL, EPSILON, TransitionGraph
|
|
19
|
+
from sofic.states import sequential_labels
|
|
20
|
+
|
|
21
|
+
TransducerSignature = tuple[tuple[Any, Any, Any, int], ...]
|
|
22
|
+
|
|
23
|
+
_PROB_DIGITS = 12
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_epsilon_transducer(channel: MealyMachine, *, name: str | None = None) -> EpsilonTransducer:
|
|
27
|
+
"""Minimize a joint-unifilar stochastic transducer to its causal states."""
|
|
28
|
+
from sofic.properties import is_unifilar_transducer
|
|
29
|
+
|
|
30
|
+
work = channel.copy()
|
|
31
|
+
if not is_unifilar_transducer(work):
|
|
32
|
+
raise StochasticValidationError(
|
|
33
|
+
"channel must be joint-unifilar (each (state, input, output) has one successor) "
|
|
34
|
+
"to build an epsilon-transducer; reconstruct from paired data with "
|
|
35
|
+
"EpsilonTransducer.from_paired_sequences instead"
|
|
36
|
+
)
|
|
37
|
+
_trim_unreachable(work)
|
|
38
|
+
partitions = _refine_partitions(work)
|
|
39
|
+
result = _quotient_transducer(work, partitions)
|
|
40
|
+
if name is not None:
|
|
41
|
+
result.name = name
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _trim_unreachable(tr: MealyMachine) -> None:
|
|
46
|
+
"""Drop states not forward-reachable from the initial states (in place)."""
|
|
47
|
+
if not tr.initial_states:
|
|
48
|
+
return
|
|
49
|
+
reachable = tr.graph.forward_reachable(set(tr.initial_states))
|
|
50
|
+
for state in list(tr.states()):
|
|
51
|
+
if state not in reachable:
|
|
52
|
+
tr.graph.nx.remove_node(state)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _refine_partitions(tr: MealyMachine) -> list[set[Any]]:
|
|
56
|
+
partitions: list[set[Any]] = [set(tr.states())]
|
|
57
|
+
changed = True
|
|
58
|
+
while changed:
|
|
59
|
+
changed = False
|
|
60
|
+
state_to_block = _state_to_block_index(partitions)
|
|
61
|
+
new_partitions: list[set[Any]] = []
|
|
62
|
+
for block in partitions:
|
|
63
|
+
subblocks = _split_block(tr, block, state_to_block)
|
|
64
|
+
if len(subblocks) > 1:
|
|
65
|
+
changed = True
|
|
66
|
+
new_partitions.extend(subblocks)
|
|
67
|
+
partitions = new_partitions
|
|
68
|
+
return partitions
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _state_to_block_index(partitions: list[set[Any]]) -> dict[Any, int]:
|
|
72
|
+
mapping: dict[Any, int] = {}
|
|
73
|
+
for index, block in enumerate(partitions):
|
|
74
|
+
for state in block:
|
|
75
|
+
mapping[state] = index
|
|
76
|
+
return mapping
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _split_block(tr: MealyMachine, block: set[Any], state_to_block: dict[Any, int]) -> list[set[Any]]:
|
|
80
|
+
signatures: dict[TransducerSignature, set[Any]] = defaultdict(set)
|
|
81
|
+
for state in block:
|
|
82
|
+
signatures[_transition_signature(tr, state, state_to_block)].add(state)
|
|
83
|
+
return list(signatures.values())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _transition_signature(tr: MealyMachine, state: Any, state_to_block: dict[Any, int]) -> TransducerSignature:
|
|
87
|
+
triples: list[tuple[Any, Any, Any, int]] = []
|
|
88
|
+
for transition in tr.graph.out_transitions(state):
|
|
89
|
+
symbol = transition.data.get(ATTR_SYMBOL, EPSILON)
|
|
90
|
+
output = transition.data.get(ATTR_OUTPUT, EPSILON)
|
|
91
|
+
prob = round(float(transition.data.get(ATTR_PROB, 1.0)), _PROB_DIGITS)
|
|
92
|
+
triples.append((symbol, output, prob, state_to_block[transition.target]))
|
|
93
|
+
return tuple(sorted(triples, key=repr))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _quotient_transducer(tr: MealyMachine, partitions: list[set[Any]]) -> EpsilonTransducer:
|
|
97
|
+
state_map: dict[Any, int] = _state_to_block_index(partitions)
|
|
98
|
+
labels = sequential_labels(len(partitions))
|
|
99
|
+
|
|
100
|
+
graph = TransitionGraph()
|
|
101
|
+
for label in labels:
|
|
102
|
+
graph.add_state(label)
|
|
103
|
+
|
|
104
|
+
inputs: set[Any] = set()
|
|
105
|
+
outputs: set[Any] = set()
|
|
106
|
+
for block_index, block in enumerate(partitions):
|
|
107
|
+
representative = next(iter(block))
|
|
108
|
+
source = labels[block_index]
|
|
109
|
+
for transition in tr.graph.out_transitions(representative):
|
|
110
|
+
symbol = transition.data.get(ATTR_SYMBOL, EPSILON)
|
|
111
|
+
output = transition.data.get(ATTR_OUTPUT, EPSILON)
|
|
112
|
+
prob = float(transition.data.get(ATTR_PROB, 1.0))
|
|
113
|
+
target = labels[state_map[transition.target]]
|
|
114
|
+
attrs: dict[str, Any] = {ATTR_SYMBOL: symbol, ATTR_PROB: prob}
|
|
115
|
+
if output is not EPSILON:
|
|
116
|
+
attrs[ATTR_OUTPUT] = output
|
|
117
|
+
graph.add_transition(source, target, **attrs)
|
|
118
|
+
if symbol is not EPSILON and symbol is not None:
|
|
119
|
+
inputs.add(symbol)
|
|
120
|
+
if output is not EPSILON and output is not None:
|
|
121
|
+
outputs.add(output)
|
|
122
|
+
|
|
123
|
+
initial_states = frozenset(labels[state_map[state]] for state in tr.initial_states if state in state_map)
|
|
124
|
+
support = initial_states or frozenset(labels)
|
|
125
|
+
mass = 1.0 / len(support)
|
|
126
|
+
initial_distribution = dict.fromkeys(support, mass)
|
|
127
|
+
|
|
128
|
+
result = EpsilonTransducer(
|
|
129
|
+
input_alphabet=frozenset(inputs),
|
|
130
|
+
output_alphabet=frozenset(outputs),
|
|
131
|
+
initial_states=initial_states,
|
|
132
|
+
initial_distribution=initial_distribution,
|
|
133
|
+
graph=graph,
|
|
134
|
+
)
|
|
135
|
+
result.validate()
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def from_joint_generator(generator: Any) -> EpsilonTransducer:
|
|
140
|
+
"""Build the ε-transducer from a generator emitting ``(input, output)`` pairs.
|
|
141
|
+
|
|
142
|
+
The joint process is first reduced to its (joint-unifilar) ε-machine, then
|
|
143
|
+
conditionalized to the channel law ``T(y, s' | s, x) = P(x, y, s' | s) /
|
|
144
|
+
P(x | s)`` before causal-state minimization.
|
|
145
|
+
"""
|
|
146
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
147
|
+
|
|
148
|
+
mealy = generator.to_mealy()
|
|
149
|
+
joint = EpsilonMachine.from_hmm(mealy)
|
|
150
|
+
channel = _channel_from_joint(joint)
|
|
151
|
+
return build_epsilon_transducer(channel)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _channel_from_joint(joint: Any) -> MealyMachine:
|
|
155
|
+
marginals: dict[tuple[Any, Any], float] = defaultdict(float)
|
|
156
|
+
inputs: set[Any] = set()
|
|
157
|
+
outputs: set[Any] = set()
|
|
158
|
+
for transition in joint.transitions():
|
|
159
|
+
emission = transition.data.get(ATTR_EMISSION)
|
|
160
|
+
pair = _as_pair(emission)
|
|
161
|
+
marginals[(transition.source, pair[0])] += float(transition.data.get(ATTR_PROB, 0.0))
|
|
162
|
+
|
|
163
|
+
channel = MealyMachine(initial_states=frozenset(joint.initial_distribution))
|
|
164
|
+
for state in joint.states():
|
|
165
|
+
channel.graph.add_state(state)
|
|
166
|
+
for transition in joint.transitions():
|
|
167
|
+
emission = transition.data.get(ATTR_EMISSION)
|
|
168
|
+
x, y = _as_pair(emission)
|
|
169
|
+
prob = float(transition.data.get(ATTR_PROB, 0.0))
|
|
170
|
+
denom = marginals[(transition.source, x)]
|
|
171
|
+
if denom <= 0.0:
|
|
172
|
+
continue
|
|
173
|
+
channel.add_transition(transition.source, transition.target, x, y, prob=prob / denom)
|
|
174
|
+
inputs.add(x)
|
|
175
|
+
outputs.add(y)
|
|
176
|
+
channel.input_alphabet = frozenset(inputs)
|
|
177
|
+
channel.output_alphabet = frozenset(outputs)
|
|
178
|
+
channel.validate()
|
|
179
|
+
return channel
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _as_pair(emission: Any) -> tuple[Any, Any]:
|
|
183
|
+
if not isinstance(emission, tuple) or len(emission) != 2:
|
|
184
|
+
raise TypeError("joint generator must emit length-2 (input, output) tuples")
|
|
185
|
+
return emission
|