a-machine 0.1.0__tar.gz

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 (63) hide show
  1. a_machine-0.1.0/.gitignore +14 -0
  2. a_machine-0.1.0/CMakeLists.txt +15 -0
  3. a_machine-0.1.0/LICENCSE.txt +7 -0
  4. a_machine-0.1.0/PKG-INFO +97 -0
  5. a_machine-0.1.0/README.md +68 -0
  6. a_machine-0.1.0/amachine/__init__.py +0 -0
  7. a_machine-0.1.0/amachine/am_causal_state.py +13 -0
  8. a_machine-0.1.0/amachine/am_cohesion.py +52 -0
  9. a_machine-0.1.0/amachine/am_create.py +294 -0
  10. a_machine-0.1.0/amachine/am_fast/__init__.py +196 -0
  11. a_machine-0.1.0/amachine/am_fast/am_fast.cpp +791 -0
  12. a_machine-0.1.0/amachine/am_fast/distance.py +137 -0
  13. a_machine-0.1.0/amachine/am_fast/json_utils.py +34 -0
  14. a_machine-0.1.0/amachine/am_generator.py +168 -0
  15. a_machine-0.1.0/amachine/am_hmm.py +1559 -0
  16. a_machine-0.1.0/amachine/am_machine.py +19 -0
  17. a_machine-0.1.0/amachine/am_msp.py +857 -0
  18. a_machine-0.1.0/amachine/am_pattern.py +21 -0
  19. a_machine-0.1.0/amachine/am_random.py +20 -0
  20. a_machine-0.1.0/amachine/am_solve.py +95 -0
  21. a_machine-0.1.0/amachine/am_substitution_algebra.py +13 -0
  22. a_machine-0.1.0/amachine/am_symbol.py +4 -0
  23. a_machine-0.1.0/amachine/am_syntagmatics.py +55 -0
  24. a_machine-0.1.0/amachine/am_transition.py +10 -0
  25. a_machine-0.1.0/amachine/am_vis.py +127 -0
  26. a_machine-0.1.0/amachine/am_vocabulary.py +69 -0
  27. a_machine-0.1.0/amachine/cli.py +6 -0
  28. a_machine-0.1.0/data/.gitkeep +0 -0
  29. a_machine-0.1.0/docs/amachine/am_causal_state.html +391 -0
  30. a_machine-0.1.0/docs/amachine/am_cohesion.html +677 -0
  31. a_machine-0.1.0/docs/amachine/am_create.html +895 -0
  32. a_machine-0.1.0/docs/amachine/am_fast/_am_fast.html +604 -0
  33. a_machine-0.1.0/docs/amachine/am_fast/distance.html +552 -0
  34. a_machine-0.1.0/docs/amachine/am_fast/json_utils.html +363 -0
  35. a_machine-0.1.0/docs/amachine/am_fast/logo.png +0 -0
  36. a_machine-0.1.0/docs/amachine/am_fast.html +693 -0
  37. a_machine-0.1.0/docs/amachine/am_generator.html +673 -0
  38. a_machine-0.1.0/docs/amachine/am_hmm.html +6245 -0
  39. a_machine-0.1.0/docs/amachine/am_machine.html +363 -0
  40. a_machine-0.1.0/docs/amachine/am_msp.html +2242 -0
  41. a_machine-0.1.0/docs/amachine/am_pattern.html +331 -0
  42. a_machine-0.1.0/docs/amachine/am_random.html +355 -0
  43. a_machine-0.1.0/docs/amachine/am_semantics.html +372 -0
  44. a_machine-0.1.0/docs/amachine/am_solve.html +466 -0
  45. a_machine-0.1.0/docs/amachine/am_substitution_algebra.html +333 -0
  46. a_machine-0.1.0/docs/amachine/am_symbol.html +285 -0
  47. a_machine-0.1.0/docs/amachine/am_syntagmatics.html +718 -0
  48. a_machine-0.1.0/docs/amachine/am_transition.html +369 -0
  49. a_machine-0.1.0/docs/amachine/am_vis.html +527 -0
  50. a_machine-0.1.0/docs/amachine/am_vocabulary.html +923 -0
  51. a_machine-0.1.0/docs/amachine/cli.html +271 -0
  52. a_machine-0.1.0/docs/amachine/logo.png +0 -0
  53. a_machine-0.1.0/docs/amachine.html +250 -0
  54. a_machine-0.1.0/docs/index.html +7 -0
  55. a_machine-0.1.0/docs/logo.png +0 -0
  56. a_machine-0.1.0/docs/search.js +46 -0
  57. a_machine-0.1.0/examples/__init__.py +0 -0
  58. a_machine-0.1.0/examples/complexity.py +44 -0
  59. a_machine-0.1.0/examples/isomorphic.py +50 -0
  60. a_machine-0.1.0/examples/random_machine.py +25 -0
  61. a_machine-0.1.0/gen_docs.py +17 -0
  62. a_machine-0.1.0/pyproject.toml +75 -0
  63. a_machine-0.1.0/uv.lock +1119 -0
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ .vscode/
4
+ build/
5
+ *.egg-info/
6
+ .cache/
7
+ *.so
8
+ dist/
9
+ data/*/
10
+ !data/.gitkeep
11
+ *.pdf
12
+ .ninja*
13
+ .skbuild*
14
+ .cmake/
@@ -0,0 +1,15 @@
1
+ cmake_minimum_required(VERSION 3.15...4.0)
2
+ project(amachine LANGUAGES CXX)
3
+
4
+ find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
5
+ find_package(nanobind CONFIG REQUIRED)
6
+
7
+ nanobind_add_module(
8
+ _am_fast
9
+ NB_STATIC
10
+ amachine/am_fast/am_fast.cpp
11
+ )
12
+
13
+ install(TARGETS _am_fast
14
+ LIBRARY DESTINATION amachine/am_fast
15
+ )
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Tyson A. Neuroth
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: a-machine
3
+ Version: 0.1.0
4
+ Summary: Construct epsilon-machines to generate symbol sequences with ground truth causal structure and information-theoretic complexity for studying neural network learning dynamics.
5
+ Author-Email: "Tyson A. Neuroth" <tyneuroth@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENCSE.txt
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: hnswlib
12
+ Requires-Dist: matplotlib
13
+ Requires-Dist: networkx
14
+ Requires-Dist: numpy
15
+ Requires-Dist: orjson
16
+ Requires-Dist: pyarrow
17
+ Requires-Dist: pygraphviz
18
+ Requires-Dist: scikit-umfpack
19
+ Requires-Dist: scipy
20
+ Requires-Dist: graphviz>=0.21
21
+ Requires-Dist: automata-lib>=9.2.0
22
+ Requires-Dist: sympy>=1.14.0
23
+ Requires-Dist: pdoc>=16.0.0
24
+ Requires-Dist: nanobind>=2.12.0
25
+ Provides-Extra: cuda
26
+ Requires-Dist: cupy-cuda13x>=14.0.1; extra == "cuda"
27
+ Requires-Dist: cuvs-cu13==26.4.*; extra == "cuda"
28
+ Description-Content-Type: text/markdown
29
+
30
+ # A-Machine
31
+
32
+ A-Machine is a library for constructing epsilon-machines[^1] and other stochastic models for generating structured symbol sequences. It was created with the goal of generating data with ground truth causal structure and information-theoretic complexity for studying neural network learning dynamics and internal representations.
33
+
34
+ This is an early work in progress. Much more to come.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ # CPU only
40
+ pip install a-machine
41
+
42
+ # With GPU support (requires CUDA 13)
43
+ pip install "a-machine[cuda]" --extra-index-url https://pypi.nvidia.com
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from amachine.am_create import random_machine
49
+
50
+ # May have multiple recurrent subgraphs, terminal states, or tranistory states
51
+ m = random_machine(
52
+ n_states=11,
53
+ symbols=[ '0', '1', '2' ],
54
+ connectedness=0.75,
55
+ randomness=0.35 )
56
+
57
+ # Collapse to the largest recurrent subgraph
58
+ m.collapse_to_largest_strongly_connected_subgraph()
59
+
60
+ # Minimize the machine -> epsilon-machine.
61
+ m.minimize()
62
+
63
+ # Entropy rate, statistical complexity, excess entropy, crypticity
64
+ print( f"h_mu : {m.h_mu()}" )
65
+ print( f"C_mu : {m.C_mu()}" )
66
+ print( f"Chi : {m.Chi()}" )
67
+
68
+ # Draw the graph
69
+ m.draw_graph( output_dir=".", show=True )
70
+ ```
71
+
72
+ ## Author
73
+
74
+ Tyson A. Neuroth
75
+
76
+ [tneuroth.gitlab.io](https://tneuroth.gitlab.io)
77
+
78
+ ## Citation
79
+
80
+ If you use this package in your research, please cite:
81
+
82
+ ```
83
+ @software{a-machine,
84
+ author = {Tyson A. Neuroth},
85
+ title = {A-Machine},
86
+ year = {2016},
87
+ url = {https://gitlab.com/tneuroth/a-machine}
88
+ }
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
94
+
95
+ ## References
96
+
97
+ [^1]: Crutchfield, James P., and Karl Young. "Inferring statistical complexity." Physical review letters 63.2 (1989): 105.
@@ -0,0 +1,68 @@
1
+ # A-Machine
2
+
3
+ A-Machine is a library for constructing epsilon-machines[^1] and other stochastic models for generating structured symbol sequences. It was created with the goal of generating data with ground truth causal structure and information-theoretic complexity for studying neural network learning dynamics and internal representations.
4
+
5
+ This is an early work in progress. Much more to come.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # CPU only
11
+ pip install a-machine
12
+
13
+ # With GPU support (requires CUDA 13)
14
+ pip install "a-machine[cuda]" --extra-index-url https://pypi.nvidia.com
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ from amachine.am_create import random_machine
20
+
21
+ # May have multiple recurrent subgraphs, terminal states, or tranistory states
22
+ m = random_machine(
23
+ n_states=11,
24
+ symbols=[ '0', '1', '2' ],
25
+ connectedness=0.75,
26
+ randomness=0.35 )
27
+
28
+ # Collapse to the largest recurrent subgraph
29
+ m.collapse_to_largest_strongly_connected_subgraph()
30
+
31
+ # Minimize the machine -> epsilon-machine.
32
+ m.minimize()
33
+
34
+ # Entropy rate, statistical complexity, excess entropy, crypticity
35
+ print( f"h_mu : {m.h_mu()}" )
36
+ print( f"C_mu : {m.C_mu()}" )
37
+ print( f"Chi : {m.Chi()}" )
38
+
39
+ # Draw the graph
40
+ m.draw_graph( output_dir=".", show=True )
41
+ ```
42
+
43
+ ## Author
44
+
45
+ Tyson A. Neuroth
46
+
47
+ [tneuroth.gitlab.io](https://tneuroth.gitlab.io)
48
+
49
+ ## Citation
50
+
51
+ If you use this package in your research, please cite:
52
+
53
+ ```
54
+ @software{a-machine,
55
+ author = {Tyson A. Neuroth},
56
+ title = {A-Machine},
57
+ year = {2016},
58
+ url = {https://gitlab.com/tneuroth/a-machine}
59
+ }
60
+ ```
61
+
62
+ ## License
63
+
64
+ MIT
65
+
66
+ ## References
67
+
68
+ [^1]: Crutchfield, James P., and Karl Young. "Inferring statistical complexity." Physical review letters 63.2 (1989): 105.
File without changes
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ @dataclass
4
+ class CausalState :
5
+ name : str
6
+ classes : set[str] = field(default_factory=set)
7
+ isomorphs : set[str] = field(default_factory=set)
8
+
9
+ def add_class( self, class_name ) :
10
+ self.classes.add( class_name )
11
+
12
+ def add_isomorph( self, state_name ) :
13
+ self.isomorphs.add( state_name )
@@ -0,0 +1,52 @@
1
+ from abc import ABC, abstractmethod
2
+ import warnings
3
+ from dataclasses import dataclass
4
+
5
+ class CohesionKernel(ABC):
6
+ """
7
+ Defines the substitution weight distributions over target symbols
8
+ given a source symbol.
9
+ """
10
+
11
+ def _normalize(self, source : str, dist: dict[str, float]) -> dict[str, float]:
12
+ wsum = sum(w for w in dist.values())
13
+ if wsum <= 1e-15:
14
+ warnings.warn( "Weight function sums to 0, implicitly mapping source with probability 1" )
15
+ res = dist.copy()
16
+ res[ source ] = 1.0
17
+ return res
18
+ return {s: w / wsum for s, w in dist.items()}
19
+
20
+ @abstractmethod
21
+ def cohesion_scores( self, source: str, symbols: set[str] ) -> dict[str, float]:
22
+ """
23
+ Maps a source symbol to a distribution of cohesion scores over other symbols.
24
+ """
25
+
26
+ @dataclass
27
+ class Uniform(CohesionKernel):
28
+ def cohesion_scores(self, source: str, symbols: set[str]) -> dict[str, float]:
29
+ return { s: 1.0 for s in symbols }
30
+
31
+ @dataclass
32
+ class Marginal(CohesionKernel):
33
+ """Per-symbol weight distribution."""
34
+ scores : dict[str, float]
35
+
36
+ def cohesion_scores(self, source: str, symbols: set[str]) -> dict[str, float]:
37
+ return {s: self.scores[s] for s in symbols }
38
+
39
+ @dataclass
40
+ class Symmetric(CohesionKernel):
41
+ """w(a->b) == w(b->a)"""
42
+ scores : dict[frozenset, float]
43
+ def cohesion_scores(self, source: str, symbols: set[str]) -> dict[str, float]:
44
+ return {s: self.scores[frozenset({source, s})] for s in symbols }
45
+
46
+ @dataclass
47
+ class Asymmetric(CohesionKernel):
48
+ """Directed pairwise scores."""
49
+ scores : dict[tuple[str,str], float]
50
+
51
+ def cohesion_scores(self, source: str, symbols: set[str]) -> dict[str, float]:
52
+ return {s: self.scores[(source, s)] for s in symbols }
@@ -0,0 +1,294 @@
1
+ from collections import defaultdict
2
+ import copy
3
+ import random
4
+
5
+ from .am_hmm import HMM
6
+ from .am_causal_state import CausalState
7
+ from .am_transition import Transition
8
+
9
+ from .am_random import uniform_dist, exp_uniform_blend
10
+
11
+ def star_join(
12
+ exit_symbol : str,
13
+ enter_symbols : list[str],
14
+ machines : list[HMM],
15
+ mode_residency_factor : float ) -> HMM :
16
+
17
+ machine = HMM()
18
+
19
+ isomorphic_groups = defaultdict(list)
20
+ for i, m in enumerate( machines ) :
21
+ if m.isoclass :
22
+ isomorphic_groups[ m.isoclass ].append( i )
23
+
24
+ # since we are merging multuple machines which might have name collision
25
+ # we need to rename the states to ensure uniqueness
26
+ def rename_state( base_name : str, g : int ) :
27
+ return f"{g}/{base_name}"
28
+
29
+ def get_gid( idx : int, isoclass : int | None ) :
30
+ return idx if isoclass is None else isoclass
31
+
32
+ machine.set_alphabet( [ exit_symbol ] )
33
+
34
+ for m in machines :
35
+ machine.extend_alphabet( alphabet=m.alphabet )
36
+
37
+ # create a connector state and connector state class
38
+
39
+ connector_state = CausalState(
40
+ name=f"/c",
41
+ classes=set({"connector"})
42
+ )
43
+
44
+ # initial states before adding each machines states
45
+ machine.set_states( [ connector_state ] )
46
+ machine.start_state = 0
47
+
48
+ # number of machines (groups of states)
49
+ n_groups = len( machines )
50
+
51
+ # make sure we have enough symbols (otherwise connector can't be unifilar)
52
+ if n_groups > len(enter_symbols) :
53
+ raise Exception(
54
+ f"Too few enter symbols given number of machines"
55
+ )
56
+
57
+ # for each given machine
58
+ for m_idx, m in enumerate( machines ) :
59
+
60
+ # default to the index of the machine in the list
61
+ m_gid = get_gid( m_idx, m.isoclass )
62
+
63
+ # give the states from this machine a class name
64
+ m_classes = {
65
+ f"m_{m_idx}",
66
+ f"isoclass_{m.isoclass}"
67
+ }
68
+
69
+ added_states = []
70
+
71
+ # create a state and extend our existing machine to include it
72
+ for s_idx, state in enumerate( m.states ) :
73
+
74
+ isomorphs=set()
75
+ if m.isoclass is not None and m.isoclass in isomorphic_groups :
76
+ for other_idx in isomorphic_groups[ m.isoclass ] :
77
+
78
+ if other_idx == m_idx :
79
+ continue
80
+
81
+ other_m = machines[ other_idx ]
82
+ isomorphs.add(
83
+ rename_state(
84
+ other_m.states[ s_idx ].name,
85
+ get_gid( other_idx, other_m.isoclass ) )
86
+ )
87
+
88
+ added_states.append(
89
+ CausalState(
90
+ name=rename_state(state.name, m_gid),
91
+ classes=( m_classes | state.classes ),
92
+ isomorphs=isomorphs
93
+ )
94
+ )
95
+
96
+ machine.extend_states( added_states )
97
+
98
+ added_transitions = []
99
+
100
+ # add all of the transitions from the machine
101
+ for tr in m.transitions :
102
+
103
+ # get the names of the states for the transition
104
+ origin_state_name = rename_state( m.states[ tr.origin_state_idx ].name, m_gid )
105
+ target_state_name = rename_state( m.states[ tr.target_state_idx ].name, m_gid )
106
+
107
+ # idx of the symbol remaped to this machines alphabet list
108
+ new_symbol_idx = machine.symbol_idx_map[ m.alphabet[ tr.symbol_idx ] ]
109
+
110
+ # create and add the new transition
111
+ added_transitions.append( Transition(
112
+ origin_state_idx=machine.state_idx_map[ origin_state_name ],
113
+ target_state_idx=machine.state_idx_map[ target_state_name ],
114
+ prob=tr.prob,
115
+ symbol_idx=new_symbol_idx
116
+ ) )
117
+
118
+ machine.extend_transitions( added_transitions )
119
+
120
+ # Add connector transitions, and adjust transition probabilities to sum to 1
121
+
122
+ # the name of the state that is the entry point to this group from the connector
123
+ m_entry_state_name = rename_state( m.states[ m.start_state ].name, m_gid )
124
+
125
+ # get the index of the entry state for this machine
126
+ m_entry_state_idx = machine.state_idx_map[ m_entry_state_name ]
127
+
128
+
129
+ # Get the within group transitions from m's entry state
130
+ # ( the probabilities will need to be adjusted )
131
+ transition_ids_from_m_entry = set()
132
+ for i, tr in enumerate( machine.transitions ) :
133
+ if tr.origin_state_idx == m_entry_state_idx :
134
+ transition_ids_from_m_entry.add( i )
135
+
136
+ n_from_entry = len( transition_ids_from_m_entry )
137
+
138
+ # Pr of staying in this group is distributed over the within group outgoing edges from the entry state
139
+ for i in transition_ids_from_m_entry :
140
+
141
+ machine.transitions[ i ] = Transition(
142
+ origin_state_idx=machine.transitions[ i ].origin_state_idx,
143
+ target_state_idx=machine.transitions[ i ].target_state_idx,
144
+ prob=mode_residency_factor / n_from_entry,
145
+ symbol_idx=machine.transitions[ i ].symbol_idx
146
+ )
147
+
148
+ # from m's entry state back to connector
149
+ escape_pr = 1.0 - mode_residency_factor
150
+
151
+ machine.extend_transitions( transitions=[
152
+ Transition(
153
+ origin_state_idx=m_entry_state_idx,
154
+ target_state_idx=machine.start_state,
155
+ prob=escape_pr,
156
+ symbol_idx=machine.symbol_idx_map[ exit_symbol ]
157
+ )
158
+ ] )
159
+
160
+ # from the connector to m's entry state
161
+ machine.extend_alphabet( alphabet=[ enter_symbols[ m_idx ] ] )
162
+
163
+ machine.extend_transitions( transitions=[
164
+ Transition(
165
+ origin_state_idx=machine.start_state,
166
+ target_state_idx=m_entry_state_idx,
167
+ prob=( 1.0 / n_groups ),
168
+ symbol_idx=machine.symbol_idx_map[ enter_symbols[ m_idx ] ]
169
+ )
170
+ ] )
171
+
172
+ return machine
173
+
174
+
175
+ def star(
176
+ exit_symbol : str,
177
+ enter_symbols : list[str],
178
+ normal_symbols : list[str],
179
+ n_modes : int = 7,
180
+ n_isomorphic : int = 2,
181
+ randomness : float = 0.3,
182
+ connectedness : float = 0.5,
183
+ residency_factor : float = 0.5,
184
+ n_normal_symbols : int = 4,
185
+ t_states_per_machine : int = 17 ) -> HMM :
186
+
187
+ if len( normal_symbols ) < n_normal_symbols*n_isomorphic :
188
+ raise ValueError( "Must have at least n_normal_symbols*n_isomorphic normal symbols" )
189
+
190
+ if len( enter_symbols ) < n_modes*n_isomorphic :
191
+ raise ValueError( "Must have at least n_modes*n_isomorphic enter symbols" )
192
+
193
+ alphabet = [ f"{normal_symbols[i]}" for i in range( 0, n_normal_symbols ) ]
194
+ iso_alphabet = [ f"{normal_symbols[i]}" for i in range( n_normal_symbols, n_normal_symbols*2 ) ]
195
+
196
+ random_machines = []
197
+
198
+ for i in range( n_modes ) :
199
+
200
+ m = random_machine(
201
+ n_states=t_states_per_machine,
202
+ symbols=alphabet,
203
+ randomness=randomness,
204
+ connectedness=connectedness )
205
+
206
+ m.collapse_to_largest_strongly_connected_subgraph()
207
+ m_iso = isomorphic_to( m, alphabet=iso_alphabet )
208
+
209
+ m.isoclass = f"{i}"
210
+ m_iso.isoclass = f"{i}"
211
+
212
+ for j, state in enumerate( m.states ) :
213
+
214
+ m.states[ j ].add_isomorph( m_iso.states[ j ].name )
215
+ m_iso.states[ j ].add_isomorph( m.states[ j ].name )
216
+
217
+ random_machines.append( m )
218
+ random_machines.append( m_iso )
219
+
220
+ mode_machine = star_join(
221
+ exit_symbol=exit_symbol,
222
+ enter_symbols=enter_symbols,
223
+ machines=random_machines,
224
+ mode_residency_factor=residency_factor
225
+ )
226
+
227
+ return mode_machine
228
+
229
+
230
+ def isomorphic_to(
231
+ m : HMM,
232
+ alphabet : list[str],
233
+ decorator : str = '@' ) -> HMM :
234
+
235
+ # make sure there are enough symbols
236
+ if len( alphabet ) < len( m.alphabet ) :
237
+ raise ValueError( "Not enough symbols in the alphabet" )
238
+
239
+ # take the as much of them as needed
240
+ alphabet_used = alphabet[ 0 : len( m.alphabet ) ]
241
+
242
+ states = [
243
+ CausalState(
244
+ name=f"{s.name}{decorator}",
245
+ classes=copy.deepcopy( s.classes )
246
+ )
247
+ for s in m.states
248
+ ]
249
+
250
+ return HMM(
251
+ states=states,
252
+ transitions=copy.deepcopy( m.transitions ),
253
+ start_state=0,
254
+ alphabet=alphabet_used
255
+ )
256
+
257
+ def random_machine(
258
+ n_states : int,
259
+ symbols : list[str],
260
+ connectedness,
261
+ randomness ) -> HMM :
262
+
263
+ states=[
264
+ CausalState( name=f"{i}" )
265
+ for i in range( n_states )
266
+ ]
267
+
268
+ n_symbols = len( symbols )
269
+ transitions = []
270
+
271
+ for state_idx, state in enumerate( states ) :
272
+
273
+ n_transitions = sum( random.random() < connectedness for _ in range( n_symbols - 1 ) ) + 1
274
+ transition_to = random.sample( range( n_states ), n_transitions )
275
+
276
+ transition_probabilities = exp_uniform_blend( n=n_transitions, alpha=randomness )
277
+ transition_symbols_indices = random.sample( range( n_symbols ), n_transitions )
278
+
279
+ for i, p in enumerate( transition_probabilities ) :
280
+ transitions.append(
281
+ Transition(
282
+ origin_state_idx=state_idx,
283
+ target_state_idx=transition_to[ i ],
284
+ prob=p,
285
+ symbol_idx=transition_symbols_indices[ i ]
286
+ )
287
+ )
288
+
289
+ return HMM(
290
+ states=states,
291
+ transitions=transitions,
292
+ start_state=0,
293
+ alphabet=symbols.copy()
294
+ )