GitLab Repo

amachine.am_create

  1from collections import defaultdict
  2import copy
  3import random
  4
  5import numpy as np
  6
  7from .am_hmm          import HMM
  8from .am_causal_state import CausalState
  9from .am_transition   import Transition
 10
 11from .am_random import uniform_dist, exp_uniform_blend, resolve_rng
 12
 13from .am_vocabulary import Vocabulary
 14
 15def star_join(
 16	exit_symbol : str, 
 17	enter_symbols : list[str],
 18	machines : list[HMM],
 19	mode_residency_factor : float ) -> HMM :
 20
 21	machine = HMM()
 22
 23	isomorphic_groups = defaultdict(list)
 24	for i, m in enumerate( machines ) :
 25		if m.isoclass is not None :
 26			isomorphic_groups[ m.isoclass ].append( i )
 27
 28	# since we are merging multuple machines which might have name collision
 29	# we need to rename the states to ensure uniqueness
 30	def rename_state( base_name : str, g : int ) :
 31		return f"{g}/{base_name}"
 32
 33	def get_gid( idx : int, isoclass : int | None ) :
 34		return idx if isoclass is None else isoclass
 35
 36	machine.set_alphabet( [ exit_symbol ] )
 37
 38	for m in machines :
 39		machine.extend_alphabet( alphabet=m.alphabet )
 40
 41	# create a connector state and connector state class
 42
 43	connector_state = CausalState(
 44		name=f"/c", 
 45		classes=set({"connector"}) 
 46	)
 47	
 48	# initial states before adding each machines states
 49	machine.set_states( [ connector_state ] )
 50	machine.start_state = 0
 51
 52	# number of machines (groups of states)
 53	n_groups = len( machines )
 54
 55	# make sure we have enough symbols (otherwise connector can't be unifilar)
 56	if n_groups > len(enter_symbols) :
 57		raise Exception(
 58			f"Too few enter symbols given number of machines"
 59		)
 60
 61	# for each given machine
 62	for m_idx, m in enumerate( machines ) : 
 63
 64		# default to the index of the machine in the list
 65		m_gid = get_gid( m_idx, m.isoclass )
 66
 67		# give the states from this machine a class name
 68		m_classes = { 
 69			f"m_{m_idx}", 
 70			f"isoclass_{m.isoclass}" 
 71		}
 72
 73		added_states = []
 74		
 75		# create a state and extend our existing machine to include it
 76		for s_idx, state in enumerate( m.states ) :
 77
 78			isomorphs=set()
 79			if m.isoclass is not None and m.isoclass in isomorphic_groups :
 80
 81				for other_idx in isomorphic_groups[ m.isoclass ] :
 82					
 83					if other_idx == m_idx : 
 84						continue
 85					
 86					other_m = machines[ other_idx ]
 87					isomorphs.add(  
 88						rename_state( 
 89							other_m.states[ s_idx ].name, 
 90							get_gid( other_idx, other_m.isoclass ) )
 91					)
 92
 93			added_states.append( 
 94				CausalState( 
 95					name=rename_state(state.name, m_gid),
 96					classes=( m_classes | state.classes ),
 97					isomorphs=isomorphs
 98				) 
 99			)
100
101		machine.extend_states( added_states )
102
103		added_transitions = []
104
105		# add all of the transitions from the machine
106		for tr in m.transitions :
107
108			# get the names of the states for the transition
109			origin_state_name = rename_state( m.states[ tr.origin_state_idx ].name, m_gid )
110			target_state_name = rename_state( m.states[ tr.target_state_idx ].name, m_gid )
111
112			# idx of the symbol remaped to this machines alphabet list
113			new_symbol_idx = machine.symbol_idx_map[ m.alphabet[ tr.symbol_idx ] ]
114
115			# create and add the new transition
116			added_transitions.append( Transition(
117				origin_state_idx=machine.state_idx_map[ origin_state_name ],
118				target_state_idx=machine.state_idx_map[ target_state_name ],
119				prob=tr.prob,
120				symbol_idx=new_symbol_idx
121			) )
122
123		machine.extend_transitions( added_transitions )
124
125		# Add connector transitions, and adjust transition probabilities to sum to 1
126
127		# the name of the state that is the entry point to this group from the connector
128		m_entry_state_name = rename_state( m.states[ m.start_state ].name, m_gid )
129
130		# get the index of the entry state for this machine
131		m_entry_state_idx = machine.state_idx_map[ m_entry_state_name ]
132
133
134		# Get the within group transitions from m's entry state
135		# ( the probabilities will need to be adjusted )
136		transition_ids_from_m_entry = set()
137		for i, tr in enumerate( machine.transitions ) : 
138			if tr.origin_state_idx == m_entry_state_idx :
139				transition_ids_from_m_entry.add( i )
140		
141		n_from_entry = len( transition_ids_from_m_entry )
142
143		# Pr of staying in this group is distributed over the within group outgoing edges from the entry state 
144		for i in transition_ids_from_m_entry :
145
146			machine.transitions[ i ] = Transition(
147				origin_state_idx=machine.transitions[ i ].origin_state_idx,
148				target_state_idx=machine.transitions[ i ].target_state_idx,
149				prob=mode_residency_factor / n_from_entry,
150				symbol_idx=machine.transitions[ i ].symbol_idx
151			)
152
153		# from m's entry state back to connector
154		escape_pr = 1.0 - mode_residency_factor
155
156		machine.extend_transitions( transitions=[
157			Transition(
158				origin_state_idx=m_entry_state_idx,
159				target_state_idx=machine.start_state,
160				prob=escape_pr,
161				symbol_idx=machine.symbol_idx_map[ exit_symbol ]
162			)
163		] )
164
165		# from the connector to m's entry state
166		machine.extend_alphabet( alphabet=[ enter_symbols[ m_idx ] ] )
167		
168		machine.extend_transitions( transitions=[
169			Transition(
170				origin_state_idx=machine.start_state,
171				target_state_idx=m_entry_state_idx,
172				prob=( 1.0 / n_groups ),
173				symbol_idx=machine.symbol_idx_map[ enter_symbols[ m_idx ] ]
174			)
175		] )
176
177	return machine
178
179def star(
180	exit_symbol          : str,
181	enter_symbols        : list[str],
182	normal_symbols       : list[str],
183	n_modes              : int = 7,
184	n_isomorphic         : int = 2,
185	randomness           : float = 0.3,
186	connectedness        : float = 0.5,
187	residency_factor     : float = 0.5,
188	n_normal_symbols     : int = 4,
189	t_states_per_machine : int = 17 )  -> HMM :
190
191	if len( normal_symbols ) < n_normal_symbols*n_isomorphic :
192		raise ValueError( "Must have at least n_normal_symbols*n_isomorphic normal symbols" )
193
194	if len( enter_symbols ) < n_modes*n_isomorphic :
195		raise ValueError( "Must have at least n_modes*n_isomorphic enter symbols" )
196
197	alphabet     = [ f"{normal_symbols[i]}" for i in range( 0, n_normal_symbols            ) ]
198	iso_alphabet = [ f"{normal_symbols[i]}" for i in range( n_normal_symbols, n_normal_symbols*2  ) ]
199
200	random_machines = []
201
202	for i in range( n_modes ) :
203
204		m = random_machine( 
205			n_states=t_states_per_machine, 
206			symbols=alphabet, 
207			randomness=randomness,
208			connectedness=connectedness ) 
209		
210		m.collapse_to_largest_strongly_connected_subgraph()
211		m_iso = isomorphic_to( m, alphabet=iso_alphabet )
212
213		m.isoclass     = f"{i}"
214		m_iso.isoclass = f"{i}"
215
216		for j, state in enumerate( m.states ) : 
217			m.states[ j ].add_isomorph( m_iso.states[ j ].name )
218			m_iso.states[ j ].add_isomorph( m.states[ j ].name )
219
220		random_machines.append( m )
221		random_machines.append( m_iso )
222
223	mode_machine = star_join(  
224		exit_symbol=exit_symbol, 
225		enter_symbols=enter_symbols,
226		machines=random_machines,
227		mode_residency_factor=residency_factor
228	)
229
230	return mode_machine
231
232
233def isomorphic_to( 
234	m : HMM, 
235	alphabet : list[str],
236	decorator : str = '@' ) -> HMM :
237
238	# make sure there are enough symbols
239	if len( alphabet ) < len( m.alphabet ) :
240		raise ValueError( "Not enough symbols in the alphabet" )
241
242	# take the as much of them as needed
243	alphabet_used = alphabet[ 0 : len( m.alphabet ) ]
244
245	states = [ 
246		CausalState( 
247			name=f"{s.name}{decorator}",
248			classes=copy.deepcopy( s.classes )
249		)
250		for s in m.states
251	]
252
253	return HMM(
254		states=states,
255		transitions=copy.deepcopy( m.transitions ),
256		start_state=0,
257		alphabet=alphabet_used
258	)
259
260def random_machine( 
261	n_states : int, 
262	symbols  : list[str], 
263	connectedness : float,
264	randomness : float,
265	random_seed : int | None = None )  -> HMM  :
266
267	if random_seed is not None:
268		py_rng = random.Random(random_seed)
269		np_rng = np.random.default_rng(random_seed)
270	else:
271		py_rng = random
272		np_rng = resolve_rng(None)
273
274	states=[
275		CausalState( name=f"{i}" )
276		for i in range( n_states  )
277	] 
278
279	n_symbols = len( symbols )
280	transitions = []
281
282	for state_idx, state in enumerate( states ) : 
283
284		n_transitions = sum( py_rng.random() < connectedness for _ in range( n_symbols - 1 ) ) + 1
285		transition_to = py_rng.sample( range( n_states ), n_transitions )
286
287		transition_probabilities = exp_uniform_blend( 
288			n=n_transitions, 
289			alpha=randomness,
290			np_rng=np_rng )
291
292		transition_symbols_indices = py_rng.sample( range( n_symbols ), n_transitions )
293
294		for i, p in enumerate( transition_probabilities ) :
295			transitions.append(
296				Transition(
297					origin_state_idx=state_idx,
298					target_state_idx=transition_to[ i ],
299					prob=p,
300					symbol_idx=transition_symbols_indices[ i ]
301				)
302			) 
303
304	return HMM( 
305		states=states,
306		transitions=transitions,
307		start_state=0,
308		alphabet=symbols.copy()
309	)
310
311def full_isomorphic_rotation(
312	isoclass_name : str, 
313	n_states : int,
314	n_base_symbols : int,
315	connectedness : float,
316	randomness : float,
317	mode_residency_factor : float,
318	random_seed : int | None = None )  -> HMM :
319
320	base_symbol_pool  = Vocabulary.digits()      + Vocabulary.geometric()
321	enter_symbol_pool = Vocabulary.greek_lower() + Vocabulary.greek_upper()
322	exit_symbol = '*'
323
324	max_n_symbols = min( len(base_symbol_pool), len(enter_symbol_pool) )
325
326	if n_base_symbols > max_n_symbols :
327		raise ValueError( f"Only up to {len(base_symbol_pool)} base symbols supported" )
328
329	alphabet = base_symbol_pool[ 0:n_base_symbols ]
330	enter_symbols = enter_symbol_pool[ 0:n_base_symbols ]
331
332	m = random_machine( 
333		n_states=n_states, 
334		symbols=alphabet, 
335		randomness=randomness,
336		connectedness=connectedness )
337
338	m.collapse_to_largest_strongly_connected_subgraph()
339	m.minimize()
340	
341	machines = []
342	for i in range( n_base_symbols ) :
343		
344		alphabet = alphabet[ -1: ] + alphabet[ 0 : n_base_symbols - 1 ]
345		im = isomorphic_to( m, alphabet=alphabet, decorator=enter_symbols[ i ] )
346		im.isoclass = isoclass_name
347		machines.append( im )
348
349	for im in machines :
350		for state in im.states :
351			
352			# get the undecorated name
353			undecorated   = state.name[ 0:-1 ]
354			my_decoration = state.name[-1]
355
356			# capture decorated names of all isomorphic states
357			for a in enter_symbols :
358
359				# self not storing self as isomorph
360				if a == my_decoration :
361					continue
362
363				decorated = undecorated + a
364				state.add_isomorph( decorated )
365
366	final_machine = star_join(  
367		exit_symbol=exit_symbol, 
368		enter_symbols=enter_symbols,
369		machines=machines,
370		mode_residency_factor=mode_residency_factor
371	)
372
373	return final_machine
374
375def unique_isomorphic(
376	isoclass_name : str,
377	n_machines : int,
378	n_states : int,
379	n_base_symbols : int,
380	connectedness : float,
381	randomness : float,
382	mode_residency_factor : float,
383	random_seed : int | None = None )  -> HMM :
384
385	base_symbol_pool  = Vocabulary.digits()      + Vocabulary.geometric()
386	enter_symbol_pool = Vocabulary.greek_lower() + Vocabulary.greek_upper()
387	exit_symbol = '*'
388
389	max_n_symbols = min( len(base_symbol_pool), len(enter_symbol_pool) )
390
391	if n_base_symbols*n_machines > max_n_symbols :
392		raise ValueError( f"Only up to {len(base_symbol_pool)} base symbols supported" )
393
394	alphabets = [
395		base_symbol_pool[ start:start + n_base_symbols ]
396		for start in range( 0, n_base_symbols*n_machines, n_base_symbols )
397	]	
398	enter_symbols = enter_symbol_pool[ 0:n_machines ]
399
400	m = random_machine( 
401		n_states=n_states, 
402		symbols=alphabets[0], 
403		randomness=randomness,
404		connectedness=connectedness )
405
406	m.isoclass = isoclass_name
407
408	m.collapse_to_largest_strongly_connected_subgraph()
409	m.minimize()	
410
411	machines = [ m ]
412	for i in range( 1, n_machines ) :
413		
414		alphabet = alphabets[ i ]
415		im = isomorphic_to( m, alphabet=alphabet, decorator=enter_symbols[ i ] )
416		im.isoclass = isoclass_name
417		machines.append( im )
418
419	for im in machines :
420		for state in im.states :
421			
422			# get the undecorated name
423			undecorated   = state.name[ 0:-1 ]
424			my_decoration = state.name[-1]
425
426			# capture decorated names of all isomorphic states
427			for a in enter_symbols :
428
429				# self not storing self as isomorph
430				if a == my_decoration :
431					continue
432
433				decorated = undecorated + a
434				state.add_isomorph( decorated )
435
436	final_machine = star_join(  
437		exit_symbol=exit_symbol, 
438		enter_symbols=enter_symbols,
439		machines=machines,
440		mode_residency_factor=mode_residency_factor
441	)
442
443	return final_machine
def star_join( exit_symbol: str, enter_symbols: list[str], machines: list[amachine.am_hmm.HMM], mode_residency_factor: float) -> amachine.am_hmm.HMM:
 16def star_join(
 17	exit_symbol : str, 
 18	enter_symbols : list[str],
 19	machines : list[HMM],
 20	mode_residency_factor : float ) -> HMM :
 21
 22	machine = HMM()
 23
 24	isomorphic_groups = defaultdict(list)
 25	for i, m in enumerate( machines ) :
 26		if m.isoclass is not None :
 27			isomorphic_groups[ m.isoclass ].append( i )
 28
 29	# since we are merging multuple machines which might have name collision
 30	# we need to rename the states to ensure uniqueness
 31	def rename_state( base_name : str, g : int ) :
 32		return f"{g}/{base_name}"
 33
 34	def get_gid( idx : int, isoclass : int | None ) :
 35		return idx if isoclass is None else isoclass
 36
 37	machine.set_alphabet( [ exit_symbol ] )
 38
 39	for m in machines :
 40		machine.extend_alphabet( alphabet=m.alphabet )
 41
 42	# create a connector state and connector state class
 43
 44	connector_state = CausalState(
 45		name=f"/c", 
 46		classes=set({"connector"}) 
 47	)
 48	
 49	# initial states before adding each machines states
 50	machine.set_states( [ connector_state ] )
 51	machine.start_state = 0
 52
 53	# number of machines (groups of states)
 54	n_groups = len( machines )
 55
 56	# make sure we have enough symbols (otherwise connector can't be unifilar)
 57	if n_groups > len(enter_symbols) :
 58		raise Exception(
 59			f"Too few enter symbols given number of machines"
 60		)
 61
 62	# for each given machine
 63	for m_idx, m in enumerate( machines ) : 
 64
 65		# default to the index of the machine in the list
 66		m_gid = get_gid( m_idx, m.isoclass )
 67
 68		# give the states from this machine a class name
 69		m_classes = { 
 70			f"m_{m_idx}", 
 71			f"isoclass_{m.isoclass}" 
 72		}
 73
 74		added_states = []
 75		
 76		# create a state and extend our existing machine to include it
 77		for s_idx, state in enumerate( m.states ) :
 78
 79			isomorphs=set()
 80			if m.isoclass is not None and m.isoclass in isomorphic_groups :
 81
 82				for other_idx in isomorphic_groups[ m.isoclass ] :
 83					
 84					if other_idx == m_idx : 
 85						continue
 86					
 87					other_m = machines[ other_idx ]
 88					isomorphs.add(  
 89						rename_state( 
 90							other_m.states[ s_idx ].name, 
 91							get_gid( other_idx, other_m.isoclass ) )
 92					)
 93
 94			added_states.append( 
 95				CausalState( 
 96					name=rename_state(state.name, m_gid),
 97					classes=( m_classes | state.classes ),
 98					isomorphs=isomorphs
 99				) 
100			)
101
102		machine.extend_states( added_states )
103
104		added_transitions = []
105
106		# add all of the transitions from the machine
107		for tr in m.transitions :
108
109			# get the names of the states for the transition
110			origin_state_name = rename_state( m.states[ tr.origin_state_idx ].name, m_gid )
111			target_state_name = rename_state( m.states[ tr.target_state_idx ].name, m_gid )
112
113			# idx of the symbol remaped to this machines alphabet list
114			new_symbol_idx = machine.symbol_idx_map[ m.alphabet[ tr.symbol_idx ] ]
115
116			# create and add the new transition
117			added_transitions.append( Transition(
118				origin_state_idx=machine.state_idx_map[ origin_state_name ],
119				target_state_idx=machine.state_idx_map[ target_state_name ],
120				prob=tr.prob,
121				symbol_idx=new_symbol_idx
122			) )
123
124		machine.extend_transitions( added_transitions )
125
126		# Add connector transitions, and adjust transition probabilities to sum to 1
127
128		# the name of the state that is the entry point to this group from the connector
129		m_entry_state_name = rename_state( m.states[ m.start_state ].name, m_gid )
130
131		# get the index of the entry state for this machine
132		m_entry_state_idx = machine.state_idx_map[ m_entry_state_name ]
133
134
135		# Get the within group transitions from m's entry state
136		# ( the probabilities will need to be adjusted )
137		transition_ids_from_m_entry = set()
138		for i, tr in enumerate( machine.transitions ) : 
139			if tr.origin_state_idx == m_entry_state_idx :
140				transition_ids_from_m_entry.add( i )
141		
142		n_from_entry = len( transition_ids_from_m_entry )
143
144		# Pr of staying in this group is distributed over the within group outgoing edges from the entry state 
145		for i in transition_ids_from_m_entry :
146
147			machine.transitions[ i ] = Transition(
148				origin_state_idx=machine.transitions[ i ].origin_state_idx,
149				target_state_idx=machine.transitions[ i ].target_state_idx,
150				prob=mode_residency_factor / n_from_entry,
151				symbol_idx=machine.transitions[ i ].symbol_idx
152			)
153
154		# from m's entry state back to connector
155		escape_pr = 1.0 - mode_residency_factor
156
157		machine.extend_transitions( transitions=[
158			Transition(
159				origin_state_idx=m_entry_state_idx,
160				target_state_idx=machine.start_state,
161				prob=escape_pr,
162				symbol_idx=machine.symbol_idx_map[ exit_symbol ]
163			)
164		] )
165
166		# from the connector to m's entry state
167		machine.extend_alphabet( alphabet=[ enter_symbols[ m_idx ] ] )
168		
169		machine.extend_transitions( transitions=[
170			Transition(
171				origin_state_idx=machine.start_state,
172				target_state_idx=m_entry_state_idx,
173				prob=( 1.0 / n_groups ),
174				symbol_idx=machine.symbol_idx_map[ enter_symbols[ m_idx ] ]
175			)
176		] )
177
178	return machine
def star( exit_symbol: str, enter_symbols: list[str], normal_symbols: list[str], n_modes: int = 7, n_isomorphic: int = 2, randomness: float = 0.3, connectedness: float = 0.5, residency_factor: float = 0.5, n_normal_symbols: int = 4, t_states_per_machine: int = 17) -> amachine.am_hmm.HMM:
180def star(
181	exit_symbol          : str,
182	enter_symbols        : list[str],
183	normal_symbols       : list[str],
184	n_modes              : int = 7,
185	n_isomorphic         : int = 2,
186	randomness           : float = 0.3,
187	connectedness        : float = 0.5,
188	residency_factor     : float = 0.5,
189	n_normal_symbols     : int = 4,
190	t_states_per_machine : int = 17 )  -> HMM :
191
192	if len( normal_symbols ) < n_normal_symbols*n_isomorphic :
193		raise ValueError( "Must have at least n_normal_symbols*n_isomorphic normal symbols" )
194
195	if len( enter_symbols ) < n_modes*n_isomorphic :
196		raise ValueError( "Must have at least n_modes*n_isomorphic enter symbols" )
197
198	alphabet     = [ f"{normal_symbols[i]}" for i in range( 0, n_normal_symbols            ) ]
199	iso_alphabet = [ f"{normal_symbols[i]}" for i in range( n_normal_symbols, n_normal_symbols*2  ) ]
200
201	random_machines = []
202
203	for i in range( n_modes ) :
204
205		m = random_machine( 
206			n_states=t_states_per_machine, 
207			symbols=alphabet, 
208			randomness=randomness,
209			connectedness=connectedness ) 
210		
211		m.collapse_to_largest_strongly_connected_subgraph()
212		m_iso = isomorphic_to( m, alphabet=iso_alphabet )
213
214		m.isoclass     = f"{i}"
215		m_iso.isoclass = f"{i}"
216
217		for j, state in enumerate( m.states ) : 
218			m.states[ j ].add_isomorph( m_iso.states[ j ].name )
219			m_iso.states[ j ].add_isomorph( m.states[ j ].name )
220
221		random_machines.append( m )
222		random_machines.append( m_iso )
223
224	mode_machine = star_join(  
225		exit_symbol=exit_symbol, 
226		enter_symbols=enter_symbols,
227		machines=random_machines,
228		mode_residency_factor=residency_factor
229	)
230
231	return mode_machine
def isomorphic_to( m: amachine.am_hmm.HMM, alphabet: list[str], decorator: str = '@') -> amachine.am_hmm.HMM:
234def isomorphic_to( 
235	m : HMM, 
236	alphabet : list[str],
237	decorator : str = '@' ) -> HMM :
238
239	# make sure there are enough symbols
240	if len( alphabet ) < len( m.alphabet ) :
241		raise ValueError( "Not enough symbols in the alphabet" )
242
243	# take the as much of them as needed
244	alphabet_used = alphabet[ 0 : len( m.alphabet ) ]
245
246	states = [ 
247		CausalState( 
248			name=f"{s.name}{decorator}",
249			classes=copy.deepcopy( s.classes )
250		)
251		for s in m.states
252	]
253
254	return HMM(
255		states=states,
256		transitions=copy.deepcopy( m.transitions ),
257		start_state=0,
258		alphabet=alphabet_used
259	)
def random_machine( n_states: int, symbols: list[str], connectedness: float, randomness: float, random_seed: int | None = None) -> amachine.am_hmm.HMM:
261def random_machine( 
262	n_states : int, 
263	symbols  : list[str], 
264	connectedness : float,
265	randomness : float,
266	random_seed : int | None = None )  -> HMM  :
267
268	if random_seed is not None:
269		py_rng = random.Random(random_seed)
270		np_rng = np.random.default_rng(random_seed)
271	else:
272		py_rng = random
273		np_rng = resolve_rng(None)
274
275	states=[
276		CausalState( name=f"{i}" )
277		for i in range( n_states  )
278	] 
279
280	n_symbols = len( symbols )
281	transitions = []
282
283	for state_idx, state in enumerate( states ) : 
284
285		n_transitions = sum( py_rng.random() < connectedness for _ in range( n_symbols - 1 ) ) + 1
286		transition_to = py_rng.sample( range( n_states ), n_transitions )
287
288		transition_probabilities = exp_uniform_blend( 
289			n=n_transitions, 
290			alpha=randomness,
291			np_rng=np_rng )
292
293		transition_symbols_indices = py_rng.sample( range( n_symbols ), n_transitions )
294
295		for i, p in enumerate( transition_probabilities ) :
296			transitions.append(
297				Transition(
298					origin_state_idx=state_idx,
299					target_state_idx=transition_to[ i ],
300					prob=p,
301					symbol_idx=transition_symbols_indices[ i ]
302				)
303			) 
304
305	return HMM( 
306		states=states,
307		transitions=transitions,
308		start_state=0,
309		alphabet=symbols.copy()
310	)
def full_isomorphic_rotation( isoclass_name: str, n_states: int, n_base_symbols: int, connectedness: float, randomness: float, mode_residency_factor: float, random_seed: int | None = None) -> amachine.am_hmm.HMM:
312def full_isomorphic_rotation(
313	isoclass_name : str, 
314	n_states : int,
315	n_base_symbols : int,
316	connectedness : float,
317	randomness : float,
318	mode_residency_factor : float,
319	random_seed : int | None = None )  -> HMM :
320
321	base_symbol_pool  = Vocabulary.digits()      + Vocabulary.geometric()
322	enter_symbol_pool = Vocabulary.greek_lower() + Vocabulary.greek_upper()
323	exit_symbol = '*'
324
325	max_n_symbols = min( len(base_symbol_pool), len(enter_symbol_pool) )
326
327	if n_base_symbols > max_n_symbols :
328		raise ValueError( f"Only up to {len(base_symbol_pool)} base symbols supported" )
329
330	alphabet = base_symbol_pool[ 0:n_base_symbols ]
331	enter_symbols = enter_symbol_pool[ 0:n_base_symbols ]
332
333	m = random_machine( 
334		n_states=n_states, 
335		symbols=alphabet, 
336		randomness=randomness,
337		connectedness=connectedness )
338
339	m.collapse_to_largest_strongly_connected_subgraph()
340	m.minimize()
341	
342	machines = []
343	for i in range( n_base_symbols ) :
344		
345		alphabet = alphabet[ -1: ] + alphabet[ 0 : n_base_symbols - 1 ]
346		im = isomorphic_to( m, alphabet=alphabet, decorator=enter_symbols[ i ] )
347		im.isoclass = isoclass_name
348		machines.append( im )
349
350	for im in machines :
351		for state in im.states :
352			
353			# get the undecorated name
354			undecorated   = state.name[ 0:-1 ]
355			my_decoration = state.name[-1]
356
357			# capture decorated names of all isomorphic states
358			for a in enter_symbols :
359
360				# self not storing self as isomorph
361				if a == my_decoration :
362					continue
363
364				decorated = undecorated + a
365				state.add_isomorph( decorated )
366
367	final_machine = star_join(  
368		exit_symbol=exit_symbol, 
369		enter_symbols=enter_symbols,
370		machines=machines,
371		mode_residency_factor=mode_residency_factor
372	)
373
374	return final_machine
def unique_isomorphic( isoclass_name: str, n_machines: int, n_states: int, n_base_symbols: int, connectedness: float, randomness: float, mode_residency_factor: float, random_seed: int | None = None) -> amachine.am_hmm.HMM:
376def unique_isomorphic(
377	isoclass_name : str,
378	n_machines : int,
379	n_states : int,
380	n_base_symbols : int,
381	connectedness : float,
382	randomness : float,
383	mode_residency_factor : float,
384	random_seed : int | None = None )  -> HMM :
385
386	base_symbol_pool  = Vocabulary.digits()      + Vocabulary.geometric()
387	enter_symbol_pool = Vocabulary.greek_lower() + Vocabulary.greek_upper()
388	exit_symbol = '*'
389
390	max_n_symbols = min( len(base_symbol_pool), len(enter_symbol_pool) )
391
392	if n_base_symbols*n_machines > max_n_symbols :
393		raise ValueError( f"Only up to {len(base_symbol_pool)} base symbols supported" )
394
395	alphabets = [
396		base_symbol_pool[ start:start + n_base_symbols ]
397		for start in range( 0, n_base_symbols*n_machines, n_base_symbols )
398	]	
399	enter_symbols = enter_symbol_pool[ 0:n_machines ]
400
401	m = random_machine( 
402		n_states=n_states, 
403		symbols=alphabets[0], 
404		randomness=randomness,
405		connectedness=connectedness )
406
407	m.isoclass = isoclass_name
408
409	m.collapse_to_largest_strongly_connected_subgraph()
410	m.minimize()	
411
412	machines = [ m ]
413	for i in range( 1, n_machines ) :
414		
415		alphabet = alphabets[ i ]
416		im = isomorphic_to( m, alphabet=alphabet, decorator=enter_symbols[ i ] )
417		im.isoclass = isoclass_name
418		machines.append( im )
419
420	for im in machines :
421		for state in im.states :
422			
423			# get the undecorated name
424			undecorated   = state.name[ 0:-1 ]
425			my_decoration = state.name[-1]
426
427			# capture decorated names of all isomorphic states
428			for a in enter_symbols :
429
430				# self not storing self as isomorph
431				if a == my_decoration :
432					continue
433
434				decorated = undecorated + a
435				state.add_isomorph( decorated )
436
437	final_machine = star_join(  
438		exit_symbol=exit_symbol, 
439		enter_symbols=enter_symbols,
440		machines=machines,
441		mode_residency_factor=mode_residency_factor
442	)
443
444	return final_machine