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,144 @@
1
+ """Constructors for canonical symbolic-shift examples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable, Sequence
6
+
7
+ from sofic.shifts.sofic_dyck import SoficDyckShift
8
+
9
+
10
+ def dyck_shift_order(
11
+ k: int = 2,
12
+ *,
13
+ call_symbols: Sequence[Hashable] | None = None,
14
+ return_symbols: Sequence[Hashable] | None = None,
15
+ ) -> SoficDyckShift:
16
+ """Dyck shift of order ``k`` from Beal, Blockelet & Dima, Example 1.
17
+
18
+ The default symbols follow the paper's notation:
19
+ ``A_c = {a1, ..., ak}`` and ``A_r = {b1, ..., bk}``.
20
+ """
21
+ if k < 1:
22
+ raise ValueError("k must be >= 1")
23
+ calls = tuple(call_symbols) if call_symbols is not None else tuple(f"a{i}" for i in range(1, k + 1))
24
+ returns = tuple(return_symbols) if return_symbols is not None else tuple(f"b{i}" for i in range(1, k + 1))
25
+ if len(calls) != k or len(returns) != k:
26
+ raise ValueError("call_symbols and return_symbols must have length k")
27
+
28
+ return _one_state_dyck_shift(calls, returns, ())
29
+
30
+
31
+ def motzkin_shift(
32
+ call_symbols: Sequence[Hashable] = ("(", "["),
33
+ return_symbols: Sequence[Hashable] = (")", "]"),
34
+ internal_symbols: Sequence[Hashable] = ("i",),
35
+ ) -> SoficDyckShift:
36
+ """Motzkin shift shown on the left of Beal, Blockelet & Dima, Fig. 1."""
37
+ return _one_state_dyck_shift(tuple(call_symbols), tuple(return_symbols), tuple(internal_symbols))
38
+
39
+
40
+ def sofic_dyck_fig1_shift() -> SoficDyckShift:
41
+ """Two-state sofic-Dyck shift shown on the right of Beal, Blockelet & Dima, Fig. 1."""
42
+ shift = SoficDyckShift(
43
+ call_alphabet=frozenset({"(", "["}),
44
+ return_alphabet=frozenset({")", "]"}),
45
+ internal_alphabet=frozenset({"i"}),
46
+ )
47
+ shift.graph.add_state("1")
48
+ shift.graph.add_state("2")
49
+
50
+ left_paren = shift.add_call_transition("1", "1", "(")
51
+ left_bracket = shift.add_call_transition("1", "1", "[")
52
+ right_paren = shift.add_return_transition("1", "1", ")")
53
+ right_bracket = shift.add_return_transition("1", "1", "]")
54
+ shift.add_internal_transition("1", "2", "i")
55
+ shift.add_internal_transition("2", "1", "i")
56
+
57
+ shift.add_matched_pair(left_paren, right_paren)
58
+ shift.add_matched_pair(left_bracket, right_bracket)
59
+ return shift
60
+
61
+
62
+ def sofic_dyck_nondeterminizable_shift() -> SoficDyckShift:
63
+ """Sofic-Dyck shift with no deterministic presentation from Beal et al., Fig. 3."""
64
+ shift = SoficDyckShift(
65
+ call_alphabet=frozenset({"a"}),
66
+ return_alphabet=frozenset({"b"}),
67
+ internal_alphabet=frozenset({"i", "j", "k"}),
68
+ )
69
+ for state in ("1", "2", "3"):
70
+ shift.graph.add_state(state)
71
+
72
+ call = shift.add_call_transition("1", "1", "a")
73
+ return_2 = shift.add_return_transition("2", "2", "b")
74
+ shift.add_return_transition("3", "3", "b")
75
+ shift.add_internal_transition("1", "2", "i")
76
+ shift.add_internal_transition("2", "1", "j")
77
+ shift.add_internal_transition("1", "3", "i")
78
+ shift.add_internal_transition("3", "1", "k")
79
+
80
+ shift.add_matched_pair(call, return_2)
81
+ return shift
82
+
83
+
84
+ def sofic_dyck_zeta_example_shift() -> SoficDyckShift:
85
+ """Sofic-Dyck shift used in Beal, Blockelet & Dima's zeta-function example."""
86
+ shift = SoficDyckShift(
87
+ call_alphabet=frozenset({"a", "a'"}),
88
+ return_alphabet=frozenset({"b", "b'"}),
89
+ internal_alphabet=frozenset({"i"}),
90
+ )
91
+ shift.graph.add_state("1")
92
+ shift.graph.add_state("2")
93
+
94
+ return_b = shift.add_return_transition("1", "1", "b")
95
+ call_a = shift.add_call_transition("1", "1", "a")
96
+ call_a_prime = shift.add_call_transition("1", "1", "a'")
97
+ return_b_prime = shift.add_return_transition("1", "1", "b'")
98
+ shift.add_internal_transition("1", "2", "i")
99
+ shift.add_internal_transition("2", "1", "i")
100
+
101
+ shift.add_matched_pair(call_a, return_b)
102
+ shift.add_matched_pair(call_a_prime, return_b_prime)
103
+ return shift
104
+
105
+
106
+ def _one_state_dyck_shift(
107
+ call_symbols: tuple[Hashable, ...],
108
+ return_symbols: tuple[Hashable, ...],
109
+ internal_symbols: tuple[Hashable, ...],
110
+ ) -> SoficDyckShift:
111
+ if len(call_symbols) != len(return_symbols):
112
+ raise ValueError("call_symbols and return_symbols must have the same length")
113
+ _require_disjoint(call_symbols, return_symbols, internal_symbols)
114
+
115
+ shift = SoficDyckShift(
116
+ call_alphabet=frozenset(call_symbols),
117
+ return_alphabet=frozenset(return_symbols),
118
+ internal_alphabet=frozenset(internal_symbols),
119
+ )
120
+ state = "1"
121
+ shift.graph.add_state(state)
122
+
123
+ call_refs = {symbol: shift.add_call_transition(state, state, symbol) for symbol in call_symbols}
124
+ return_refs = {symbol: shift.add_return_transition(state, state, symbol) for symbol in return_symbols}
125
+ for symbol in internal_symbols:
126
+ shift.add_internal_transition(state, state, symbol)
127
+ for call_symbol, return_symbol in zip(call_symbols, return_symbols, strict=True):
128
+ shift.add_matched_pair(call_refs[call_symbol], return_refs[return_symbol])
129
+ return shift
130
+
131
+
132
+ def _require_disjoint(*symbol_groups: tuple[Hashable, ...]) -> None:
133
+ symbols = tuple(symbol for group in symbol_groups for symbol in group)
134
+ if len(frozenset(symbols)) != len(symbols):
135
+ raise ValueError("call, return, and internal symbols must be distinct")
136
+
137
+
138
+ __all__ = [
139
+ "dyck_shift_order",
140
+ "motzkin_shift",
141
+ "sofic_dyck_fig1_shift",
142
+ "sofic_dyck_nondeterminizable_shift",
143
+ "sofic_dyck_zeta_example_shift",
144
+ ]
sofic/exceptions.py ADDED
@@ -0,0 +1,33 @@
1
+ """Exceptions raised by sofic."""
2
+
3
+
4
+ class SoficError(Exception):
5
+ """Base class for sofic errors."""
6
+
7
+
8
+ class SoficValidationError(SoficError):
9
+ """Raised when a model fails structural or semantic validation."""
10
+
11
+
12
+ class NonDeterministicError(SoficValidationError):
13
+ """Raised when a DFA determinism invariant is violated."""
14
+
15
+
16
+ class StochasticValidationError(SoficValidationError):
17
+ """Raised when probability masses are invalid."""
18
+
19
+
20
+ class UnifilarityError(SoficValidationError):
21
+ """Raised when a unifilarity invariant is violated."""
22
+
23
+
24
+ class QuasiStochasticValidationError(SoficValidationError):
25
+ """Raised when quasi-stochastic invariants are violated."""
26
+
27
+
28
+ class LumpabilityError(SoficValidationError):
29
+ """Raised when a partition is not strongly lumpable for a model."""
30
+
31
+
32
+ class InfiniteTransductionError(SoficError):
33
+ """Raised when a finite input has infinitely many transducer outputs."""
@@ -0,0 +1,115 @@
1
+ """Stochastic and quasiprobabilistic generators."""
2
+
3
+ from sofic.generators.base import HiddenMarkovModel, QuasiStochasticModel, StochasticModel
4
+ from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
5
+ from sofic.generators.block_convergence import BlockConvergenceDiagram, BlockConvergenceEstimates
6
+ from sofic.generators.block_entropy import BlockEntropyDiagram, BlockEntropyEstimates
7
+ from sofic.generators.channel_measures import (
8
+ channel_statistical_complexity,
9
+ driven_entropy_rate,
10
+ )
11
+ from sofic.generators.directional_flow import (
12
+ directed_information,
13
+ independent_pair_generator,
14
+ intrinsic_information_flow,
15
+ shared_information_flow,
16
+ synergistic_information_flow,
17
+ transfer_entropy,
18
+ )
19
+ from sofic.generators.epsilon_inference import cssr, spectral, subtree_merge
20
+ from sofic.generators.epsilon_machine import EpsilonMachine
21
+ from sofic.generators.epsilon_transducer import EpsilonTransducer
22
+ from sofic.generators.lumping import LumpabilityError, is_lumpable, lump, normalize_partition
23
+ from sofic.generators.markov import MarkovChain
24
+ from sofic.generators.mealy import MealyHMM
25
+ from sofic.generators.minimal_generative_model import (
26
+ FunctionalGenerativeModel,
27
+ GacsKornerGenerativeModel,
28
+ MinimalGenerativeModel,
29
+ WynerGenerativeModel,
30
+ functional_generative_model,
31
+ gacs_korner_generative_model,
32
+ minimal_generative_model,
33
+ wyner_generative_model,
34
+ )
35
+ from sofic.generators.mixed_state import MixedState, MixedStatePresentation
36
+ from sofic.generators.moore import MooreHMM
37
+ from sofic.generators.nmachine import NMachine
38
+ from sofic.generators.pfa import ProbabilisticFiniteAutomaton
39
+ from sofic.generators.quasi_realization import QuasiRealization
40
+ from sofic.generators.stack_hmm import HiddenMarkovStackModel
41
+ from sofic.generators.stack_inference import (
42
+ fit_stack_hmm_mle,
43
+ learn_stack_hmm_papni,
44
+ stack_cssr,
45
+ stack_subtree_merge,
46
+ )
47
+ from sofic.generators.topological_epsilon_enumeration import (
48
+ count_topological_epsilon_machines,
49
+ epsilon_machine_to_idfa_string,
50
+ idfa_string_to_epsilon_machine,
51
+ is_canonical_topological_epsilon,
52
+ is_minimal_idfa,
53
+ is_strongly_connected_idfa,
54
+ is_topological_epsilon_string,
55
+ iter_topological_epsilon_machines,
56
+ iter_topological_epsilon_strings,
57
+ )
58
+
59
+ __all__ = [
60
+ "BidirectionalEpsilonMachine",
61
+ "BlockEntropyDiagram",
62
+ "BlockEntropyEstimates",
63
+ "BlockConvergenceDiagram",
64
+ "BlockConvergenceEstimates",
65
+ "EpsilonMachine",
66
+ "EpsilonTransducer",
67
+ "LumpabilityError",
68
+ "channel_statistical_complexity",
69
+ "driven_entropy_rate",
70
+ "cssr",
71
+ "is_lumpable",
72
+ "lump",
73
+ "normalize_partition",
74
+ "spectral",
75
+ "subtree_merge",
76
+ "fit_stack_hmm_mle",
77
+ "learn_stack_hmm_papni",
78
+ "stack_cssr",
79
+ "stack_subtree_merge",
80
+ "directed_information",
81
+ "independent_pair_generator",
82
+ "intrinsic_information_flow",
83
+ "shared_information_flow",
84
+ "synergistic_information_flow",
85
+ "transfer_entropy",
86
+ "FunctionalGenerativeModel",
87
+ "GacsKornerGenerativeModel",
88
+ "HiddenMarkovModel",
89
+ "HiddenMarkovStackModel",
90
+ "MarkovChain",
91
+ "MealyHMM",
92
+ "MinimalGenerativeModel",
93
+ "MixedState",
94
+ "MixedStatePresentation",
95
+ "MooreHMM",
96
+ "NMachine",
97
+ "ProbabilisticFiniteAutomaton",
98
+ "QuasiRealization",
99
+ "QuasiStochasticModel",
100
+ "StochasticModel",
101
+ "WynerGenerativeModel",
102
+ "count_topological_epsilon_machines",
103
+ "epsilon_machine_to_idfa_string",
104
+ "functional_generative_model",
105
+ "gacs_korner_generative_model",
106
+ "idfa_string_to_epsilon_machine",
107
+ "is_canonical_topological_epsilon",
108
+ "is_minimal_idfa",
109
+ "is_strongly_connected_idfa",
110
+ "is_topological_epsilon_string",
111
+ "iter_topological_epsilon_machines",
112
+ "iter_topological_epsilon_strings",
113
+ "minimal_generative_model",
114
+ "wyner_generative_model",
115
+ ]
@@ -0,0 +1,94 @@
1
+ """Dit-backed word-distribution measures for block curves.
2
+
3
+ Leaf module shared by :mod:`sofic.generators.block_entropy` and
4
+ :mod:`sofic.generators.block_convergence`. It depends only on ``dit`` (via
5
+ :func:`sofic.generators.measures.require_dit`) and must not import either
6
+ block module, so that both can depend on it without a circular import.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from sofic.generators.epsilon_machine import EpsilonMachine
15
+
16
+ _TOL = 1e-15
17
+
18
+
19
+ def _require_dit() -> Any:
20
+ from sofic.generators.measures import require_dit
21
+
22
+ return require_dit("block convergence measures")
23
+
24
+
25
+ def _positional_rvs(length: int) -> list[list[int]]:
26
+ return [[index] for index in range(length)]
27
+
28
+
29
+ def _word_distribution(distribution: dict[tuple[Any, ...], float]) -> Any:
30
+ dit = _require_dit()
31
+ if not distribution:
32
+ return dit.Distribution([()], [1.0])
33
+ total = sum(float(prob) for prob in distribution.values())
34
+ if total <= _TOL:
35
+ return dit.Distribution([()], [1.0])
36
+ outcomes = list(distribution.keys())
37
+ probs = [float(distribution[outcome]) / total for outcome in outcomes]
38
+ dist = dit.Distribution(outcomes, probs)
39
+ dist.set_rv_names([f"X_{index}" for index in range(len(outcomes[0]))])
40
+ return dist
41
+
42
+
43
+ def _block_word_distribution(machine: EpsilonMachine, length: int) -> Any:
44
+ if length == 0:
45
+ dit = _require_dit()
46
+ return dit.Distribution([()], [1.0])
47
+ return _word_distribution(machine.word_probabilities(length))
48
+
49
+
50
+ def _block_total_correlation(dist: Any, length: int, block_entropy: float, h1: float) -> float:
51
+ if length <= 1:
52
+ return 0.0
53
+ _require_dit()
54
+ from dit.multivariate import total_correlation
55
+
56
+ return float(total_correlation(dist, rvs=_positional_rvs(length)))
57
+
58
+
59
+ def _block_residual_entropy(dist: Any, length: int) -> float:
60
+ if length == 0:
61
+ return 0.0
62
+ _require_dit()
63
+ from dit.multivariate import residual_entropy
64
+
65
+ return float(residual_entropy(dist, rvs=_positional_rvs(length)))
66
+
67
+
68
+ def _block_coinformation(dist: Any, length: int, block_entropy: float) -> float:
69
+ if length <= 1:
70
+ return 0.0
71
+ _require_dit()
72
+ from dit.multivariate import coinformation
73
+
74
+ return float(coinformation(dist, rvs=_positional_rvs(length)))
75
+
76
+
77
+ def _block_caekl(dist: Any, length: int) -> float:
78
+ if length <= 1:
79
+ return 0.0
80
+ _require_dit()
81
+ from dit.multivariate import caekl_mutual_information
82
+
83
+ return float(caekl_mutual_information(dist, rvs=_positional_rvs(length)))
84
+
85
+
86
+ def residual_entropy_from_distribution(distribution: dict[tuple[Any, ...], float]) -> float:
87
+ """Dit-backed residual entropy for a word distribution."""
88
+ if not distribution:
89
+ return 0.0
90
+ length = len(next(iter(distribution)))
91
+ if length == 0:
92
+ return 0.0
93
+ dist = _word_distribution(distribution)
94
+ return _block_residual_entropy(dist, length)
@@ -0,0 +1,104 @@
1
+ """Alternative complexity measures for epsilon-machines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+
10
+ from sofic.generators.stochastic import shannon_entropy
11
+ from sofic.graph import ATTR_PROB
12
+ from sofic.properties import transition_matrix
13
+
14
+ if TYPE_CHECKING:
15
+ from sofic.generators.epsilon_machine import EpsilonMachine
16
+
17
+
18
+ def structural_information(machine: EpsilonMachine) -> float:
19
+ """Asymptotic structural information (equals excess entropy for finite-state sources).
20
+
21
+ For a canonical epsilon-machine, :math:`I_{\\mathrm{struct}} = \\lim_{L\\to\\infty}
22
+ H[S_L \\mid X_{0:L}] = E` (Feldman & Crutchfield, 1998; Ellison et al., 2009).
23
+ """
24
+ return machine.excess_entropy()
25
+
26
+
27
+ def thermodynamic_depth(machine: EpsilonMachine) -> float:
28
+ """Thermodynamic depth: stationary average mean first-passage time to causal states.
29
+
30
+ Uses the hidden-state transition matrix (summing over emissions) and the
31
+ stationary causal-state distribution (Shalizi & Crutchfield, 1999).
32
+ """
33
+ pi = machine.stationary_distribution()
34
+ states = list(machine.states())
35
+ if not states:
36
+ return 0.0
37
+ index = {state: i for i, state in enumerate(states)}
38
+ n = len(states)
39
+ transition, _ = transition_matrix(machine, attr=ATTR_PROB, states=states)
40
+
41
+ pi_vec = np.asarray([pi[index[state]] for state in states], dtype=float)
42
+ pi_vec = pi_vec / pi_vec.sum()
43
+
44
+ depths = np.zeros(n, dtype=float)
45
+ for target in range(n):
46
+ depths[target] = _mean_first_passage_time(transition, pi_vec, target)
47
+
48
+ return float(np.dot(pi_vec, depths))
49
+
50
+
51
+ def spectral_complexity(machine: EpsilonMachine) -> float:
52
+ """Spectral entropy of nontrivial mixed-state transition eigenvalues.
53
+
54
+ Builds the recurrent mixed-state presentation and summarizes the modulus
55
+ spectrum of its transition operator (Riechers & Crutchfield, 2017).
56
+ """
57
+ from sofic.generators.mixed_state_construction import build_mixed_state_presentation
58
+
59
+ msp = build_mixed_state_presentation(machine)
60
+ states = tuple(msp.recurrent_states)
61
+ if len(states) <= 1:
62
+ return 0.0
63
+
64
+ matrix, _ = transition_matrix(msp, attr=ATTR_PROB, states=states)
65
+
66
+ eigenvalues = np.linalg.eigvals(matrix)
67
+ moduli = np.sort(np.abs(eigenvalues))[::-1]
68
+ nontrivial = moduli[moduli < 1.0 - 1e-9]
69
+ if nontrivial.size == 0:
70
+ return 0.0
71
+ weights = nontrivial / nontrivial.sum()
72
+ return shannon_entropy(weights)
73
+
74
+
75
+ def _mean_first_passage_time(transition: np.ndarray, start: np.ndarray, target: int) -> float:
76
+ n = transition.shape[0]
77
+ if n == 0:
78
+ return 0.0
79
+ if n == 1:
80
+ return 0.0
81
+
82
+ # Fundamental-matrix formula for MFPT from i to j.
83
+ pi = _stationary_vector(transition)
84
+ if pi is None:
85
+ pi = np.ones(n) / n
86
+ fundamental = np.linalg.inv(np.eye(n) - transition + np.outer(np.ones(n), pi))
87
+ mfpt = np.zeros(n, dtype=float)
88
+ for i in range(n):
89
+ if i == target:
90
+ mfpt[i] = 0.0
91
+ elif pi[target] <= 0.0:
92
+ mfpt[i] = math.inf
93
+ else:
94
+ mfpt[i] = (fundamental[target, target] - fundamental[i, target]) / pi[target]
95
+ return float(start @ mfpt)
96
+
97
+
98
+ def _stationary_vector(transition: np.ndarray) -> np.ndarray | None:
99
+ from sofic.generators.stationary import stationary_distribution_from_transition
100
+
101
+ try:
102
+ return stationary_distribution_from_transition(transition)
103
+ except Exception:
104
+ return None