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/shifts/textile.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Textile systems (Nasu 1995): the machine form of a sliding block code.
|
|
2
|
+
|
|
3
|
+
A textile system encodes a factor map between subshifts as a pair of labelings
|
|
4
|
+
over a shared edge graph -- one reading the domain (input) symbols, one the range
|
|
5
|
+
(output) symbols. This is Nasu's formalization (*Textile Systems for Endomorphisms
|
|
6
|
+
and Automorphisms of the Shift*, Memoirs AMS 546, 1995); operationally it is a
|
|
7
|
+
topological transducer whose input labeling, when right-resolving, induces a
|
|
8
|
+
:class:`~sofic.shifts.sliding_block_code.SlidingBlockCode` from the input shift to
|
|
9
|
+
the output shift.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import deque
|
|
15
|
+
from collections.abc import Hashable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from sofic.graph import ATTR_OUTPUT, ATTR_SYMBOL, EPSILON
|
|
19
|
+
from sofic.shifts.sliding_block_code import SlidingBlockCode
|
|
20
|
+
from sofic.shifts.sofic import SoficShift
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TextileSystem:
|
|
24
|
+
"""A shared edge graph with paired input/output labels (a topological transducer)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, transducer: Any) -> None:
|
|
27
|
+
self._transducer = transducer
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_transducer(cls, transducer: Any) -> TextileSystem:
|
|
31
|
+
"""Wrap a (topological) Mealy machine as a textile system."""
|
|
32
|
+
return cls(transducer.copy())
|
|
33
|
+
|
|
34
|
+
def to_transducer(self) -> Any:
|
|
35
|
+
"""Return the underlying Mealy machine."""
|
|
36
|
+
return self._transducer.copy()
|
|
37
|
+
|
|
38
|
+
def to_sofic_relation(self) -> Any:
|
|
39
|
+
"""Return the product-alphabet subshift of paired labels."""
|
|
40
|
+
from sofic.shifts.sofic_relation import SoficRelation
|
|
41
|
+
|
|
42
|
+
return SoficRelation.from_transducer(self._transducer)
|
|
43
|
+
|
|
44
|
+
def input_shift(self) -> SoficShift:
|
|
45
|
+
"""Return the input subshift (``p`` labeling)."""
|
|
46
|
+
return self.to_sofic_relation().input_shift()
|
|
47
|
+
|
|
48
|
+
def output_shift(self) -> SoficShift:
|
|
49
|
+
"""Return the output subshift (``q`` labeling)."""
|
|
50
|
+
return self.to_sofic_relation().output_shift()
|
|
51
|
+
|
|
52
|
+
def induced_code(self, *, max_window: int = 4) -> SlidingBlockCode:
|
|
53
|
+
"""Return the induced sliding block code (memory only), if it has finite window.
|
|
54
|
+
|
|
55
|
+
Searches windows of length ``1 .. max_window``; for each, the code is
|
|
56
|
+
well-defined when every readable input window determines a unique output
|
|
57
|
+
on its final position. Raises :class:`ValueError` if the input labeling is
|
|
58
|
+
not right-resolving with a finite window up to ``max_window``.
|
|
59
|
+
"""
|
|
60
|
+
transducer = self._transducer
|
|
61
|
+
input_alphabet = frozenset(transducer.input_alphabet) or {
|
|
62
|
+
transition.data.get(ATTR_SYMBOL) for transition in transducer.transitions()
|
|
63
|
+
}
|
|
64
|
+
for window in range(1, max_window + 1):
|
|
65
|
+
block_map = self._window_block_map(transducer, window)
|
|
66
|
+
if block_map is not None:
|
|
67
|
+
return SlidingBlockCode(
|
|
68
|
+
block_map,
|
|
69
|
+
memory=window - 1,
|
|
70
|
+
anticipation=0,
|
|
71
|
+
input_alphabet=input_alphabet,
|
|
72
|
+
output_alphabet={value for value in block_map.values()},
|
|
73
|
+
)
|
|
74
|
+
raise ValueError(f"no finite-memory induced code up to window {max_window}")
|
|
75
|
+
|
|
76
|
+
def _window_block_map(self, transducer: Any, window: int) -> dict[tuple[Any, ...], Any] | None:
|
|
77
|
+
# For each readable input word of `window` symbols, collect the set of
|
|
78
|
+
# possible outputs on the final edge across all paths reading that word.
|
|
79
|
+
outputs: dict[tuple[Any, ...], set[Any]] = {}
|
|
80
|
+
start_states = list(transducer.states())
|
|
81
|
+
for start in start_states:
|
|
82
|
+
queue: deque[tuple[Hashable, tuple[Any, ...], Any]] = deque([(start, (), None)])
|
|
83
|
+
while queue:
|
|
84
|
+
state, word, last_output = queue.popleft()
|
|
85
|
+
if len(word) == window:
|
|
86
|
+
outputs.setdefault(word, set()).add(last_output)
|
|
87
|
+
continue
|
|
88
|
+
for transition in transducer.graph.out_transitions(state):
|
|
89
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
90
|
+
if symbol is None or symbol is EPSILON:
|
|
91
|
+
continue
|
|
92
|
+
emitted = transition.data.get(ATTR_OUTPUT)
|
|
93
|
+
queue.append((transition.target, (*word, symbol), emitted))
|
|
94
|
+
if not outputs:
|
|
95
|
+
return None
|
|
96
|
+
block_map: dict[tuple[Any, ...], Any] = {}
|
|
97
|
+
for word, possible in outputs.items():
|
|
98
|
+
if len(possible) != 1:
|
|
99
|
+
return None
|
|
100
|
+
value = next(iter(possible))
|
|
101
|
+
if value is None or value is EPSILON:
|
|
102
|
+
return None
|
|
103
|
+
block_map[word] = value
|
|
104
|
+
return block_map
|
sofic/shifts/tmc.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Topological Markov chains."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sofic.generators.mealy import MealyHMM
|
|
11
|
+
from sofic.graph import ATTR_MULTIPLICITY
|
|
12
|
+
from sofic.shifts.base import SymbolicModel
|
|
13
|
+
from sofic.shifts.sofic import SoficShift
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TopologicalMarkovChain(SymbolicModel):
|
|
17
|
+
"""Adjacency-matrix presentation with edge multiplicities."""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_adjacency(
|
|
21
|
+
cls, matrix: np.ndarray, symbol_alphabet: frozenset[Any] | None = None, **kwargs: Any
|
|
22
|
+
) -> TopologicalMarkovChain:
|
|
23
|
+
from sofic.shifts.tmc_construction import from_adjacency
|
|
24
|
+
|
|
25
|
+
return from_adjacency(matrix, symbol_alphabet)
|
|
26
|
+
|
|
27
|
+
def to_sofic_shift(self) -> SoficShift:
|
|
28
|
+
from sofic.shifts.tmc_construction import to_sofic_shift
|
|
29
|
+
|
|
30
|
+
return to_sofic_shift(self)
|
|
31
|
+
|
|
32
|
+
def topological_entropy(self) -> float:
|
|
33
|
+
from sofic.shifts.tmc_construction import topological_entropy
|
|
34
|
+
|
|
35
|
+
return topological_entropy(self)
|
|
36
|
+
|
|
37
|
+
def validate(self) -> None:
|
|
38
|
+
super().validate()
|
|
39
|
+
for transition in self.transitions():
|
|
40
|
+
mult = transition.data.get(ATTR_MULTIPLICITY, 1)
|
|
41
|
+
self._require(isinstance(mult, (int, float)) and mult >= 1, "multiplicity must be >= 1")
|
|
42
|
+
|
|
43
|
+
def parry_measure(self) -> MealyHMM:
|
|
44
|
+
from sofic.shifts.parry_construction import parry_measure
|
|
45
|
+
|
|
46
|
+
return parry_measure(self)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""TMC construction and Sofic conversion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from sofic.graph import ATTR_MULTIPLICITY, ATTR_SYMBOL, TransitionGraph
|
|
10
|
+
from sofic.shifts.algorithms import adjacency_matrix, topological_entropy_from_matrix
|
|
11
|
+
from sofic.shifts.sofic import SoficShift
|
|
12
|
+
from sofic.shifts.tmc import TopologicalMarkovChain
|
|
13
|
+
from sofic.states import sequential_labels
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def from_adjacency(
|
|
17
|
+
matrix: np.ndarray,
|
|
18
|
+
symbol_alphabet: frozenset[Any] | None = None,
|
|
19
|
+
) -> TopologicalMarkovChain:
|
|
20
|
+
arr = np.asarray(matrix, dtype=float)
|
|
21
|
+
n = arr.shape[0]
|
|
22
|
+
states = sequential_labels(n)
|
|
23
|
+
graph = TransitionGraph()
|
|
24
|
+
for state in states:
|
|
25
|
+
graph.add_state(state)
|
|
26
|
+
symbols = tuple(symbol_alphabet) if symbol_alphabet else tuple(range(n))
|
|
27
|
+
for i in range(n):
|
|
28
|
+
for j in range(n):
|
|
29
|
+
count = int(arr[i, j])
|
|
30
|
+
if count <= 0:
|
|
31
|
+
continue
|
|
32
|
+
symbol = symbols[j % len(symbols)] if symbol_alphabet else j
|
|
33
|
+
graph.add_transition(states[i], states[j], **{ATTR_SYMBOL: symbol, ATTR_MULTIPLICITY: count})
|
|
34
|
+
return TopologicalMarkovChain(
|
|
35
|
+
graph=graph,
|
|
36
|
+
symbol_alphabet=symbol_alphabet if symbol_alphabet is not None else frozenset(symbols),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_sofic_shift(tmc: TopologicalMarkovChain) -> SoficShift:
|
|
41
|
+
graph = TransitionGraph()
|
|
42
|
+
for state in tmc.states():
|
|
43
|
+
graph.add_state(state)
|
|
44
|
+
for transition in tmc.transitions():
|
|
45
|
+
multiplicity = int(transition.data.get(ATTR_MULTIPLICITY, 1))
|
|
46
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
47
|
+
for _ in range(max(multiplicity, 1)):
|
|
48
|
+
graph.add_transition(
|
|
49
|
+
transition.source,
|
|
50
|
+
transition.target,
|
|
51
|
+
**{ATTR_SYMBOL: symbol},
|
|
52
|
+
)
|
|
53
|
+
return SoficShift(graph=graph, symbol_alphabet=tmc.symbol_alphabet)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def topological_entropy(model: TopologicalMarkovChain | SoficShift) -> float:
|
|
57
|
+
matrix, _ = adjacency_matrix(model)
|
|
58
|
+
return topological_entropy_from_matrix(matrix)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Topological information anatomy of a sofic shift: the MME split of ``h_top``.
|
|
2
|
+
|
|
3
|
+
Topological entropy ``h_top`` is the measure-independent counterpart of the metric
|
|
4
|
+
entropy ``h_mu``: by the variational principle ``h_top = sup_nu h_nu``, attained by
|
|
5
|
+
the **measure of maximal entropy** (MME) -- the Parry measure of the shift
|
|
6
|
+
(Parry 1964). This module evaluates the *information anatomy* (James et al. 2013)
|
|
7
|
+
of the observed symbol process at that MME, giving a topological analogue of the
|
|
8
|
+
``h_mu = b_mu + r_mu`` split:
|
|
9
|
+
|
|
10
|
+
h_top = b_top + r_top, b_top := b_{mu*}, r_top := r_{mu*},
|
|
11
|
+
|
|
12
|
+
where ``mu*`` is the MME. Concretely:
|
|
13
|
+
|
|
14
|
+
- ``h_top = h_{mu*} = log2(lambda)`` (Perron eigenvalue of the right-resolving
|
|
15
|
+
presentation's adjacency matrix),
|
|
16
|
+
- ``b_top = I[X_0 : X_{1:} | X_{:0}]`` under ``mu*`` (bound information), and
|
|
17
|
+
- ``r_top = H[X_0 | X_{:0}, X_{1:}]`` under ``mu*`` -- the **erasure entropy rate**
|
|
18
|
+
(Verdu & Weissman 2008) of the MME (ephemeral information).
|
|
19
|
+
|
|
20
|
+
Because the MME and the observed symbol process are intrinsic to the sofic shift,
|
|
21
|
+
``b_top`` and ``r_top`` are functions of the shift space (up to alphabet
|
|
22
|
+
relabeling / 1-block conjugacy), refining ``h_top`` the way ``b_mu`` / ``r_mu``
|
|
23
|
+
refine ``h_mu``. They are exact: the MME is a finite Markov chain on the
|
|
24
|
+
presentation, so the bidirectional-epsilon-machine anatomy is closed-form (no
|
|
25
|
+
Monte-Carlo, no finite-block truncation).
|
|
26
|
+
|
|
27
|
+
Pipeline (see :func:`topological_anatomy`):
|
|
28
|
+
|
|
29
|
+
1. Put the presentation in right-resolving (unifilar) form
|
|
30
|
+
(:func:`_right_resolving`) -- required so ``h_top`` is exact rather than the
|
|
31
|
+
path-overcount of a nondeterministic presentation.
|
|
32
|
+
2. Build the Parry MME as a labeled :class:`~sofic.generators.mealy.MealyHMM`
|
|
33
|
+
(:func:`parry_measure_sofic`).
|
|
34
|
+
3. Minimize to the causal presentation
|
|
35
|
+
(:meth:`~sofic.generators.epsilon_machine.EpsilonMachine.from_hmm`), pair it
|
|
36
|
+
with its time reverse
|
|
37
|
+
(:meth:`~sofic.generators.epsilon_machine.EpsilonMachine.to_bidirectional`),
|
|
38
|
+
and read the anatomy
|
|
39
|
+
(:meth:`~sofic.generators.bidirectional_epsilon_machine.BidirectionalEpsilonMachine.information_anatomy`).
|
|
40
|
+
|
|
41
|
+
References: Parry (1964), *Intrinsic Markov chains*; James, Ellison & Crutchfield
|
|
42
|
+
(2013), *Anatomy of a bit*; Verdu & Weissman (2008), *The information lost in
|
|
43
|
+
erasures*.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from typing import TYPE_CHECKING
|
|
49
|
+
|
|
50
|
+
from sofic.exceptions import UnifilarityError
|
|
51
|
+
from sofic.graph import ATTR_SYMBOL
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from sofic.generators.mealy import MealyHMM
|
|
55
|
+
from sofic.shifts.sofic import SoficShift
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _dedup_symbol_edges(shift: SoficShift) -> SoficShift:
|
|
59
|
+
"""Return a copy of ``shift`` with duplicate ``(source, target, symbol)`` edges merged.
|
|
60
|
+
|
|
61
|
+
The Fischer-cover construction maps every original transition onto a
|
|
62
|
+
follower-class edge without deduplication, so merging classes can create
|
|
63
|
+
several identical ``(source, target, symbol)`` edges. Those are the *same*
|
|
64
|
+
labeled edge and must be counted once for the adjacency/Perron entropy to be
|
|
65
|
+
correct; genuine right-resolving parallelism (same ``(source, target)``,
|
|
66
|
+
*different* symbols) is preserved.
|
|
67
|
+
"""
|
|
68
|
+
from sofic.shifts.sofic import SoficShift
|
|
69
|
+
|
|
70
|
+
result = SoficShift(symbol_alphabet=shift.symbol_alphabet)
|
|
71
|
+
for state in shift.states():
|
|
72
|
+
result.graph.add_state(state)
|
|
73
|
+
seen: set[tuple[object, object, object]] = set()
|
|
74
|
+
for transition in shift.transitions():
|
|
75
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
76
|
+
key = (transition.source, transition.target, symbol)
|
|
77
|
+
if key in seen:
|
|
78
|
+
continue
|
|
79
|
+
seen.add(key)
|
|
80
|
+
result.add_transition(transition.source, transition.target, symbol)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _right_resolving(shift: SoficShift) -> SoficShift:
|
|
85
|
+
"""Return a right-resolving (unifilar) presentation of ``shift``.
|
|
86
|
+
|
|
87
|
+
If ``shift`` is already unifilar it is returned unchanged. Otherwise the right
|
|
88
|
+
Fischer cover (:meth:`~sofic.shifts.covers.RightFischerCover.from_sofic`) is
|
|
89
|
+
built and its duplicate labeled edges merged (:func:`_dedup_symbol_edges`).
|
|
90
|
+
The cover construction uses a bounded follower language, so it is not
|
|
91
|
+
guaranteed to determinize every presentation; if the result is still not
|
|
92
|
+
unifilar a :class:`~sofic.exceptions.UnifilarityError` is raised asking for a
|
|
93
|
+
right-resolving input.
|
|
94
|
+
"""
|
|
95
|
+
if shift.is_unifilar():
|
|
96
|
+
return shift
|
|
97
|
+
from sofic.shifts.covers import RightFischerCover
|
|
98
|
+
|
|
99
|
+
cover = _dedup_symbol_edges(RightFischerCover.from_sofic(shift))
|
|
100
|
+
if not cover.is_unifilar():
|
|
101
|
+
raise UnifilarityError(
|
|
102
|
+
"could not derive a right-resolving presentation of the sofic shift "
|
|
103
|
+
"automatically; supply a unifilar (right-resolving) SoficShift"
|
|
104
|
+
)
|
|
105
|
+
return cover
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parry_measure_sofic(shift: SoficShift) -> MealyHMM:
|
|
109
|
+
"""Return the measure of maximal entropy (Parry measure) as a labeled MealyHMM.
|
|
110
|
+
|
|
111
|
+
Puts ``shift`` in right-resolving form (:func:`_right_resolving`) and builds
|
|
112
|
+
the Parry chain ``P[i, j] = A[i, j] v_j / (lambda v_i)`` with stationary
|
|
113
|
+
``pi ~ u * v`` from the Perron data of the adjacency matrix ``A`` (Parry 1964),
|
|
114
|
+
preserving each edge's emitted symbol. Its entropy rate is
|
|
115
|
+
``log2(lambda) = h_top`` and its observed process is the shift's MME symbol
|
|
116
|
+
process.
|
|
117
|
+
"""
|
|
118
|
+
from sofic.shifts.parry_construction import parry_measure
|
|
119
|
+
|
|
120
|
+
return parry_measure(_right_resolving(shift))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def topological_anatomy(shift: SoficShift) -> dict[str, float]:
|
|
124
|
+
"""Return the topological information anatomy ``{h_top, b_top, r_top, excess_entropy}``.
|
|
125
|
+
|
|
126
|
+
Evaluates the metric anatomy (James et al. 2013) of the observed symbol
|
|
127
|
+
process at the measure of maximal entropy (:func:`parry_measure_sofic`):
|
|
128
|
+
|
|
129
|
+
- ``h_top`` -- topological entropy ``= log2(lambda)`` (the MME entropy rate),
|
|
130
|
+
- ``b_top`` -- MME bound information ``I[X_0 : X_{1:} | X_{:0}]``,
|
|
131
|
+
- ``r_top`` -- MME ephemeral information ``H[X_0 | X_{:0}, X_{1:}]`` (the MME
|
|
132
|
+
erasure entropy rate, Verdu & Weissman 2008),
|
|
133
|
+
- ``excess_entropy`` -- MME excess entropy ``E = I[past : future]``,
|
|
134
|
+
|
|
135
|
+
with ``h_top = b_top + r_top`` exactly. Requires ``dit`` for the anatomy
|
|
136
|
+
entropies.
|
|
137
|
+
"""
|
|
138
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
139
|
+
|
|
140
|
+
parry = parry_measure_sofic(shift)
|
|
141
|
+
if not list(parry.states()):
|
|
142
|
+
return {"h_top": 0.0, "b_top": 0.0, "r_top": 0.0, "excess_entropy": 0.0}
|
|
143
|
+
|
|
144
|
+
anatomy = EpsilonMachine.from_hmm(parry).information_anatomy()
|
|
145
|
+
return {
|
|
146
|
+
"h_top": float(anatomy["entropy_rate"]),
|
|
147
|
+
"b_top": float(anatomy["bound_mu"]),
|
|
148
|
+
"r_top": float(anatomy["ephemeral_mu"]),
|
|
149
|
+
"excess_entropy": float(anatomy["excess_entropy"]),
|
|
150
|
+
}
|
sofic/states.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Sequential capital-letter state labels (A, B, C, ...)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sequential_labels(count: int, *, start: int = 0) -> tuple[str, ...]:
|
|
9
|
+
"""Return ``count`` state labels starting at ``chr(ord('A') + start)``."""
|
|
10
|
+
if count < 0:
|
|
11
|
+
raise ValueError("count must be non-negative")
|
|
12
|
+
if start < 0 or start + count > 26:
|
|
13
|
+
raise ValueError("sequential_labels supports at most 26 states A–Z")
|
|
14
|
+
return tuple(chr(ord("A") + start + index) for index in range(count))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def letter_label_index(label: Hashable) -> int | None:
|
|
18
|
+
"""Map a single capital letter ``A``–``Z`` to 0–25, else ``None``."""
|
|
19
|
+
if isinstance(label, str) and len(label) == 1 and "A" <= label <= "Z":
|
|
20
|
+
return ord(label) - ord("A")
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def next_sequential_label_index(existing: Iterable[Hashable]) -> int:
|
|
25
|
+
"""Index of the next unused capital letter after ``existing`` labels."""
|
|
26
|
+
indices = [index for item in existing if (index := letter_label_index(item)) is not None]
|
|
27
|
+
return max(indices, default=-1) + 1
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Hypothesis strategies for sofic models.
|
|
2
|
+
|
|
3
|
+
These helpers live outside the main package imports so Hypothesis remains a
|
|
4
|
+
test-only dependency. Install ``sofic[test]`` to use them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from functools import cache
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sofic.automata.dfa import DFA
|
|
14
|
+
from sofic.automata.icdfa import icdfa_string_to_dfa, iter_icdfa_empty_strings
|
|
15
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
16
|
+
from sofic.generators.topological_epsilon_enumeration import (
|
|
17
|
+
idfa_string_to_epsilon_machine,
|
|
18
|
+
iter_topological_epsilon_strings,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DEFAULT_ALPHABET = ("0", "1")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def dfas(
|
|
25
|
+
*,
|
|
26
|
+
alphabet: Sequence[Any] = DEFAULT_ALPHABET,
|
|
27
|
+
min_states: int = 1,
|
|
28
|
+
max_states: int = 3,
|
|
29
|
+
) -> Any:
|
|
30
|
+
"""Return a strategy for complete initially-connected DFAs.
|
|
31
|
+
|
|
32
|
+
The DFA transition structures are drawn from the canonical ICDFA
|
|
33
|
+
enumeration, and accepting states are drawn independently.
|
|
34
|
+
"""
|
|
35
|
+
st = _hypothesis_strategies()
|
|
36
|
+
symbols = _validate_alphabet(alphabet)
|
|
37
|
+
_validate_state_bounds(min_states=min_states, max_states=max_states)
|
|
38
|
+
k = len(symbols)
|
|
39
|
+
|
|
40
|
+
@st.composite
|
|
41
|
+
def strategy(draw: Any) -> DFA:
|
|
42
|
+
n = draw(st.integers(min_value=min_states, max_value=max_states))
|
|
43
|
+
transitions = draw(st.sampled_from(_icdfa_transition_strings(k, n)))
|
|
44
|
+
final_states = draw(st.frozensets(st.integers(min_value=0, max_value=n - 1)))
|
|
45
|
+
return icdfa_string_to_dfa(
|
|
46
|
+
transitions,
|
|
47
|
+
symbols,
|
|
48
|
+
n=n,
|
|
49
|
+
k=k,
|
|
50
|
+
final_states=final_states,
|
|
51
|
+
symbol_order=symbols,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return strategy()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def epsilon_machines(
|
|
58
|
+
*,
|
|
59
|
+
alphabet: Sequence[Any] = DEFAULT_ALPHABET,
|
|
60
|
+
min_states: int = 1,
|
|
61
|
+
max_states: int = 3,
|
|
62
|
+
max_pool: int | None = None,
|
|
63
|
+
) -> Any:
|
|
64
|
+
"""Return a strategy for topological epsilon-machines.
|
|
65
|
+
|
|
66
|
+
Machines are drawn from the canonical topological epsilon-machine
|
|
67
|
+
enumeration and use the enumeration module's uniform row probabilities.
|
|
68
|
+
|
|
69
|
+
When ``max_pool`` is set, only the first ``max_pool`` enumerated transition
|
|
70
|
+
strings per ``(k, n)`` are retained. Use this for large alphabets (e.g.
|
|
71
|
+
``k = 3``) where the full enumeration is infeasible.
|
|
72
|
+
"""
|
|
73
|
+
st = _hypothesis_strategies()
|
|
74
|
+
symbols = _validate_alphabet(alphabet)
|
|
75
|
+
_validate_state_bounds(min_states=min_states, max_states=max_states)
|
|
76
|
+
if max_pool is not None and max_pool < 1:
|
|
77
|
+
raise ValueError("max_pool must be positive when provided")
|
|
78
|
+
k = len(symbols)
|
|
79
|
+
state_counts: tuple[int, ...] = ()
|
|
80
|
+
for n in range(min_states, max_states + 1):
|
|
81
|
+
if max_pool is not None:
|
|
82
|
+
pool = _bounded_topological_epsilon_transition_strings(k, n, max_pool)
|
|
83
|
+
else:
|
|
84
|
+
pool = _topological_epsilon_transition_strings(k, n)
|
|
85
|
+
if pool:
|
|
86
|
+
state_counts += (n,)
|
|
87
|
+
if not state_counts:
|
|
88
|
+
raise ValueError("no topological epsilon-machine strings exist for the requested bounds")
|
|
89
|
+
|
|
90
|
+
@st.composite
|
|
91
|
+
def strategy(draw: Any) -> EpsilonMachine:
|
|
92
|
+
n = draw(st.sampled_from(state_counts))
|
|
93
|
+
if max_pool is not None:
|
|
94
|
+
pool = _bounded_topological_epsilon_transition_strings(k, n, max_pool)
|
|
95
|
+
else:
|
|
96
|
+
pool = _topological_epsilon_transition_strings(k, n)
|
|
97
|
+
transitions = draw(st.sampled_from(pool))
|
|
98
|
+
return idfa_string_to_epsilon_machine(transitions, n=n, k=k, alphabet=symbols)
|
|
99
|
+
|
|
100
|
+
return strategy()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _hypothesis_strategies() -> Any:
|
|
104
|
+
try:
|
|
105
|
+
from hypothesis import strategies as st
|
|
106
|
+
except ImportError as exc: # pragma: no cover - exercised only without test extra
|
|
107
|
+
raise ImportError("sofic.testing.strategies requires Hypothesis; install sofic[test].") from exc
|
|
108
|
+
return st
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _validate_alphabet(alphabet: Sequence[Any]) -> tuple[Any, ...]:
|
|
112
|
+
symbols = tuple(alphabet)
|
|
113
|
+
if not symbols:
|
|
114
|
+
raise ValueError("alphabet must be non-empty")
|
|
115
|
+
try:
|
|
116
|
+
unique = frozenset(symbols)
|
|
117
|
+
except TypeError as exc:
|
|
118
|
+
raise ValueError("alphabet symbols must be hashable") from exc
|
|
119
|
+
if len(unique) != len(symbols):
|
|
120
|
+
raise ValueError("alphabet symbols must be unique")
|
|
121
|
+
return symbols
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _validate_state_bounds(*, min_states: int, max_states: int) -> None:
|
|
125
|
+
if min_states < 1:
|
|
126
|
+
raise ValueError("min_states must be positive")
|
|
127
|
+
if max_states < 1:
|
|
128
|
+
raise ValueError("max_states must be positive")
|
|
129
|
+
if min_states > max_states:
|
|
130
|
+
raise ValueError("min_states must be less than or equal to max_states")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@cache
|
|
134
|
+
def _icdfa_transition_strings(k: int, n: int) -> tuple[tuple[int, ...], ...]:
|
|
135
|
+
return tuple(iter_icdfa_empty_strings(k, n))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@cache
|
|
139
|
+
def _topological_epsilon_transition_strings(k: int, n: int) -> tuple[tuple[int, ...], ...]:
|
|
140
|
+
return tuple(iter_topological_epsilon_strings(k, n))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@cache
|
|
144
|
+
def _bounded_topological_epsilon_transition_strings(
|
|
145
|
+
k: int,
|
|
146
|
+
n: int,
|
|
147
|
+
max_pool: int,
|
|
148
|
+
) -> tuple[tuple[int, ...], ...]:
|
|
149
|
+
pool: list[tuple[int, ...]] = []
|
|
150
|
+
for transitions in iter_topological_epsilon_strings(k, n):
|
|
151
|
+
pool.append(tuple(transitions))
|
|
152
|
+
if len(pool) >= max_pool:
|
|
153
|
+
break
|
|
154
|
+
return tuple(pool)
|
sofic/viz/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Visualization for sofic models (Graphviz, TikZ, information diagrams)."""
|
|
2
|
+
|
|
3
|
+
from sofic.viz.graphviz import draw, model_to_graphviz, model_to_svg
|
|
4
|
+
from sofic.viz.idiagram import plot_information_diagram
|
|
5
|
+
from sofic.viz.tikz import compile_tikz, draw_tikz, model_to_tikz, model_to_tikz_image
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"compile_tikz",
|
|
9
|
+
"draw",
|
|
10
|
+
"draw_tikz",
|
|
11
|
+
"model_to_graphviz",
|
|
12
|
+
"model_to_svg",
|
|
13
|
+
"model_to_tikz",
|
|
14
|
+
"model_to_tikz_image",
|
|
15
|
+
"plot_information_diagram",
|
|
16
|
+
]
|