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,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,8 @@
1
+ """Optional Hypothesis strategies for sofic models."""
2
+
3
+ from sofic.testing.strategies import dfas, epsilon_machines
4
+
5
+ __all__ = [
6
+ "dfas",
7
+ "epsilon_machines",
8
+ ]
@@ -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
+ ]