amachine.am_create.am_structured_isomorphic_permutations_of
1from typing import Literal 2import warnings 3import math 4import itertools 5 6import numpy as np 7 8from ..am_hmm import HMM 9from ..am_random import ( 10 resolve_rng, 11 unique_random_permutations, 12 unique_random_combinations 13) 14 15from ..am_random import resolve_rng 16 17from ..am_vocabulary import Vocabulary 18from ..am_structured_symbol_set import StructuredSymbolSet 19 20from .am_isomorphic_to_with_category_permutations import isomorphic_to_with_category_permutations 21 22def structured_isomorphic_permutations_of( 23 m : HMM, 24 symbol_set : StructuredSymbolSet, 25 n_permutations : int, 26 permutation_group : Literal[ "cyclic_product", "symmetric" ] = "symmetric", 27 selection_mode : Literal[ "sequential", "random", "cohesive" ] = "cohesive", 28 invariant_categories : set[int] | None = None, 29 np_rng : np.random.Generator | None = None ) -> list[HMM]: 30 31 np_rng = resolve_rng( np_rng ) 32 33 if permutation_group not in { "cyclic_product", "symmetric" } : 34 raise ValueError( f"Invalid permutation group {permutation_group}, options are cyclic_product or symmetric" ) 35 36 if selection_mode not in { "sequential", "random", "cohesive" } : 37 raise ValueError( f"Invalid selection_mode {selection_mode} options are sequential, random, or cohesive" ) 38 39 if invariant_categories is None : 40 invariant_categories = set() 41 42 if n_permutations == 1 : 43 return [ m ] 44 45 categories_to_permute = symbol_set.categories - invariant_categories 46 ordered_categories = sorted(symbol_set.categories) 47 48 category_sizes = symbol_set.symbol_counts_by_category() 49 category_symbols = symbol_set.category_symbols() 50 51 print( f"Creating {n_permutations} total isomorphic HMMs" ) 52 53 m_isos = [] 54 55 enter_symbol_pool = list( set( 56 Vocabulary.digits() 57 + Vocabulary.letters_lower() 58 + Vocabulary.letters_upper() 59 + Vocabulary.greek_lower() 60 + Vocabulary.greek_upper() ) - set( m.alphabet ) ) 61 62 effective_sizes = [ 63 category_sizes[c] if c not in invariant_categories else 1 64 for c in ordered_categories 65 ] 66 67 if permutation_group == "cyclic_product" : 68 69 def shifts_to_permutations( shift_sequence ) : 70 permutation_sequence = [] 71 for shifts in shift_sequence : 72 permutations = {} 73 for c, shift in shifts.items() : 74 permutations[ c ] = {} 75 for s in category_symbols[ c ] : 76 idx = symbol_set.symbols.index( s ) 77 new_idx = ( idx + shift ) % len( category_symbols[ c ] ) 78 permutations[ c ][ s ] = category_symbols[ c ][ new_idx ] 79 permutation_sequence.append( permutations ) 80 return permutation_sequence 81 82 if selection_mode == "sequential" : 83 shift_sequence = [ 84 { 85 c : i if c in categories_to_permute else 0 86 for c in ordered_categories 87 } 88 for i in range( 1, n_permutations ) 89 ] 90 91 permutation_sequence = shifts_to_permutations( shift_sequence ) 92 93 elif selection_mode == "random": 94 95 combinations = unique_random_combinations( 96 bin_sizes=effective_sizes, 97 n_samples=n_permutations-1, 98 np_rng=np_rng 99 ) 100 101 shift_sequence = [ 102 { c : p[c] for c in ordered_categories } 103 for p in combinations 104 ] 105 106 permutation_sequence = shifts_to_permutations( shift_sequence ) 107 108 elif selection_mode == "cohesive" : 109 110 pool_size = min( math.prod( effective_sizes)-1, 1_000_000 ) 111 112 pool = unique_random_combinations( 113 bin_sizes=effective_sizes, 114 n_samples=pool_size, 115 np_rng=np_rng 116 ) 117 118 bigrams = [] 119 for tra in m.transitions : 120 for trb in m.transitions : 121 if tra == trb : 122 continue 123 if tra.target_state_idx == trb.origin_state_idx : 124 bigrams.append( ( 125 m.alphabet[ tra.symbol_idx ], 126 m.alphabet[ trb.symbol_idx ] ) ) 127 128 category_symbols = symbol_set.category_symbols() 129 130 scored_combinations = [] 131 for combination in pool : 132 score = 0 133 for a, b in bigrams : 134 135 c_a = symbol_set.symbol_categories[ a ] 136 c_b = symbol_set.symbol_categories[ b ] 137 138 cat_a = category_symbols[ c_a ] 139 cat_b = category_symbols[ c_b ] 140 141 new_a = cat_a[ ( cat_a.index( a ) + combination[ c_a ] ) % len( cat_a ) ] 142 new_b = cat_b[ ( cat_b.index( b ) + combination[ c_b ] ) % len( cat_b ) ] 143 144 score += symbol_set.symbol_cohesion[ 145 symbol_set.symbols.index( new_a ), 146 symbol_set.symbols.index( new_b ) 147 ] 148 149 scored_combinations.append( ( score, combination ) ) 150 151 scored_combinations = sorted( scored_combinations, reverse=True ) 152 scored_combinations = scored_combinations[ 0 : n_permutations-1 ] 153 154 shift_sequence = [ 155 { c : p[c] for c in ordered_categories } 156 for score, p in scored_combinations 157 ] 158 159 permutation_sequence = shifts_to_permutations( shift_sequence ) 160 161 elif permutation_group == "symmetric" : 162 163 164 if selection_mode == "sequential": 165 166 category_iters = [] 167 for c in ordered_categories: 168 if c in categories_to_permute: 169 category_iters.append(itertools.permutations(range(len(category_symbols[c])))) 170 else: 171 category_iters.append([tuple(range(len(category_symbols[c])))]) 172 173 product_iter = itertools.product(*category_iters) 174 175 try: 176 next(product_iter) 177 except StopIteration: 178 pass 179 180 permutation_sequence = [] 181 for _ in range(n_permutations-1): 182 try: 183 indices_tuple = next(product_iter) 184 except StopIteration: 185 warnings.warn(f"Requested {n_permutations} permutations, but only {len(permutation_sequence)} symmetric combinations asides from identity exist.") 186 break 187 188 permutations = {} 189 for c, perm_indices in enumerate(indices_tuple): 190 permutations[c] = {} 191 for old_idx, new_idx in enumerate(perm_indices): 192 old_sym = category_symbols[c][old_idx] 193 new_sym = category_symbols[c][new_idx] 194 permutations[c][old_sym] = new_sym 195 196 permutation_sequence.append(permutations) 197 198 elif selection_mode == "random": 199 200 pool_size = min( math.prod( effective_sizes)-1, 1_000_000 ) 201 202 index_permutation_sequence = unique_random_permutations( 203 bin_sizes=effective_sizes, 204 n_samples=pool_size, 205 np_rng=np_rng ) 206 207 permutation_sequence = [ 208 { 209 c: { 210 category_symbols[c][old_idx] : category_symbols[c][new_idx] 211 for old_idx, new_idx in index_perm.items() 212 } 213 for c, index_perm in index_permutation.items() 214 } 215 for index_permutation in index_permutation_sequence 216 ] 217 218 elif selection_mode == "cohesive": 219 220 bigram_indices = [] 221 for tra in m.transitions: 222 for trb in m.transitions: 223 if tra != trb and tra.target_state_idx == trb.origin_state_idx: 224 c_a = symbol_set.symbol_categories[ m.alphabet[tra.symbol_idx] ] 225 c_b = symbol_set.symbol_categories[ m.alphabet[trb.symbol_idx] ] 226 227 # Store the category and the original index within that category 228 orig_idx_a = category_symbols[c_a].index( m.alphabet[tra.symbol_idx] ) 229 orig_idx_b = category_symbols[c_b].index( m.alphabet[trb.symbol_idx] ) 230 231 bigram_indices.append( (c_a, orig_idx_a, c_b, orig_idx_b) ) 232 233 def calculate_score(perm_state): 234 score = 0 235 for c_a, idx_a, c_b, idx_b in bigram_indices: 236 237 new_sym_a = category_symbols[c_a][ perm_state[c_a][idx_a] ] 238 new_sym_b = category_symbols[c_b][ perm_state[c_b][idx_b] ] 239 240 global_idx_a = symbol_set.symbols.index(new_sym_a) 241 global_idx_b = symbol_set.symbols.index(new_sym_b) 242 243 score += symbol_set.symbol_cohesion[global_idx_a, global_idx_b] 244 return score 245 246 permutation_sequence = [] 247 248 # Hill Climbing Loop 249 for _ in range(n_permutations-1): 250 251 # Start with a random permutation state for each category 252 current_perm = { 253 c: list(np_rng.permutation(len(category_symbols[c]))) if c in categories_to_permute else list(range(len(category_symbols[c]))) 254 for c in ordered_categories 255 } 256 257 current_score = calculate_score(current_perm) 258 improved = True 259 260 # Keep optimizing until we hit a local maximum 261 while improved: 262 improved = False 263 264 # Try swapping every possible pair in every active category 265 for c in categories_to_permute: 266 n_sym = len(category_symbols[c]) 267 for i in range(n_sym): 268 for j in range(i + 1, n_sym): 269 270 # Perform swap 271 current_perm[c][i], current_perm[c][j] = current_perm[c][j], current_perm[c][i] 272 273 new_score = calculate_score(current_perm) 274 275 if new_score > current_score: 276 current_score = new_score 277 improved = True 278 else: 279 # Revert swap if it didn't improve the score 280 current_perm[c][i], current_perm[c][j] = current_perm[c][j], current_perm[c][i] 281 282 # Translate the optimized integer lists back into the expected dictionary format 283 final_permutation = {} 284 for c in ordered_categories : 285 final_permutation[c] = { 286 category_symbols[c][old_idx] : category_symbols[c][new_idx] 287 for old_idx, new_idx in enumerate(current_perm[c]) 288 } 289 290 permutation_sequence.append(final_permutation) 291 292 for idx, permutations in enumerate( permutation_sequence ) : 293 294 m_iso = isomorphic_to_with_category_permutations( 295 m=m, 296 symbol_set=symbol_set, 297 permutations=permutations, 298 decorator=f"@{idx}" 299 ) 300 301 m_isos.append( m_iso ) 302 303 all_machines = [ m ] + m_isos 304 305 for ma in all_machines : 306 for mb in all_machines : 307 if ma != mb : 308 for j, state in enumerate( ma.states ) : 309 ma.states[ j ].add_isomorph( mb.states[ j ].name ) 310 mb.states[ j ].add_isomorph( ma.states[ j ].name ) 311 312 return all_machines
def
structured_isomorphic_permutations_of( m: amachine.am_hmm.HMM, symbol_set: amachine.am_structured_symbol_set.StructuredSymbolSet, n_permutations: int, permutation_group: Literal['cyclic_product', 'symmetric'] = 'symmetric', selection_mode: Literal['sequential', 'random', 'cohesive'] = 'cohesive', invariant_categories: set[int] | None = None, np_rng: numpy.random._generator.Generator | None = None) -> list[amachine.am_hmm.HMM]:
23def structured_isomorphic_permutations_of( 24 m : HMM, 25 symbol_set : StructuredSymbolSet, 26 n_permutations : int, 27 permutation_group : Literal[ "cyclic_product", "symmetric" ] = "symmetric", 28 selection_mode : Literal[ "sequential", "random", "cohesive" ] = "cohesive", 29 invariant_categories : set[int] | None = None, 30 np_rng : np.random.Generator | None = None ) -> list[HMM]: 31 32 np_rng = resolve_rng( np_rng ) 33 34 if permutation_group not in { "cyclic_product", "symmetric" } : 35 raise ValueError( f"Invalid permutation group {permutation_group}, options are cyclic_product or symmetric" ) 36 37 if selection_mode not in { "sequential", "random", "cohesive" } : 38 raise ValueError( f"Invalid selection_mode {selection_mode} options are sequential, random, or cohesive" ) 39 40 if invariant_categories is None : 41 invariant_categories = set() 42 43 if n_permutations == 1 : 44 return [ m ] 45 46 categories_to_permute = symbol_set.categories - invariant_categories 47 ordered_categories = sorted(symbol_set.categories) 48 49 category_sizes = symbol_set.symbol_counts_by_category() 50 category_symbols = symbol_set.category_symbols() 51 52 print( f"Creating {n_permutations} total isomorphic HMMs" ) 53 54 m_isos = [] 55 56 enter_symbol_pool = list( set( 57 Vocabulary.digits() 58 + Vocabulary.letters_lower() 59 + Vocabulary.letters_upper() 60 + Vocabulary.greek_lower() 61 + Vocabulary.greek_upper() ) - set( m.alphabet ) ) 62 63 effective_sizes = [ 64 category_sizes[c] if c not in invariant_categories else 1 65 for c in ordered_categories 66 ] 67 68 if permutation_group == "cyclic_product" : 69 70 def shifts_to_permutations( shift_sequence ) : 71 permutation_sequence = [] 72 for shifts in shift_sequence : 73 permutations = {} 74 for c, shift in shifts.items() : 75 permutations[ c ] = {} 76 for s in category_symbols[ c ] : 77 idx = symbol_set.symbols.index( s ) 78 new_idx = ( idx + shift ) % len( category_symbols[ c ] ) 79 permutations[ c ][ s ] = category_symbols[ c ][ new_idx ] 80 permutation_sequence.append( permutations ) 81 return permutation_sequence 82 83 if selection_mode == "sequential" : 84 shift_sequence = [ 85 { 86 c : i if c in categories_to_permute else 0 87 for c in ordered_categories 88 } 89 for i in range( 1, n_permutations ) 90 ] 91 92 permutation_sequence = shifts_to_permutations( shift_sequence ) 93 94 elif selection_mode == "random": 95 96 combinations = unique_random_combinations( 97 bin_sizes=effective_sizes, 98 n_samples=n_permutations-1, 99 np_rng=np_rng 100 ) 101 102 shift_sequence = [ 103 { c : p[c] for c in ordered_categories } 104 for p in combinations 105 ] 106 107 permutation_sequence = shifts_to_permutations( shift_sequence ) 108 109 elif selection_mode == "cohesive" : 110 111 pool_size = min( math.prod( effective_sizes)-1, 1_000_000 ) 112 113 pool = unique_random_combinations( 114 bin_sizes=effective_sizes, 115 n_samples=pool_size, 116 np_rng=np_rng 117 ) 118 119 bigrams = [] 120 for tra in m.transitions : 121 for trb in m.transitions : 122 if tra == trb : 123 continue 124 if tra.target_state_idx == trb.origin_state_idx : 125 bigrams.append( ( 126 m.alphabet[ tra.symbol_idx ], 127 m.alphabet[ trb.symbol_idx ] ) ) 128 129 category_symbols = symbol_set.category_symbols() 130 131 scored_combinations = [] 132 for combination in pool : 133 score = 0 134 for a, b in bigrams : 135 136 c_a = symbol_set.symbol_categories[ a ] 137 c_b = symbol_set.symbol_categories[ b ] 138 139 cat_a = category_symbols[ c_a ] 140 cat_b = category_symbols[ c_b ] 141 142 new_a = cat_a[ ( cat_a.index( a ) + combination[ c_a ] ) % len( cat_a ) ] 143 new_b = cat_b[ ( cat_b.index( b ) + combination[ c_b ] ) % len( cat_b ) ] 144 145 score += symbol_set.symbol_cohesion[ 146 symbol_set.symbols.index( new_a ), 147 symbol_set.symbols.index( new_b ) 148 ] 149 150 scored_combinations.append( ( score, combination ) ) 151 152 scored_combinations = sorted( scored_combinations, reverse=True ) 153 scored_combinations = scored_combinations[ 0 : n_permutations-1 ] 154 155 shift_sequence = [ 156 { c : p[c] for c in ordered_categories } 157 for score, p in scored_combinations 158 ] 159 160 permutation_sequence = shifts_to_permutations( shift_sequence ) 161 162 elif permutation_group == "symmetric" : 163 164 165 if selection_mode == "sequential": 166 167 category_iters = [] 168 for c in ordered_categories: 169 if c in categories_to_permute: 170 category_iters.append(itertools.permutations(range(len(category_symbols[c])))) 171 else: 172 category_iters.append([tuple(range(len(category_symbols[c])))]) 173 174 product_iter = itertools.product(*category_iters) 175 176 try: 177 next(product_iter) 178 except StopIteration: 179 pass 180 181 permutation_sequence = [] 182 for _ in range(n_permutations-1): 183 try: 184 indices_tuple = next(product_iter) 185 except StopIteration: 186 warnings.warn(f"Requested {n_permutations} permutations, but only {len(permutation_sequence)} symmetric combinations asides from identity exist.") 187 break 188 189 permutations = {} 190 for c, perm_indices in enumerate(indices_tuple): 191 permutations[c] = {} 192 for old_idx, new_idx in enumerate(perm_indices): 193 old_sym = category_symbols[c][old_idx] 194 new_sym = category_symbols[c][new_idx] 195 permutations[c][old_sym] = new_sym 196 197 permutation_sequence.append(permutations) 198 199 elif selection_mode == "random": 200 201 pool_size = min( math.prod( effective_sizes)-1, 1_000_000 ) 202 203 index_permutation_sequence = unique_random_permutations( 204 bin_sizes=effective_sizes, 205 n_samples=pool_size, 206 np_rng=np_rng ) 207 208 permutation_sequence = [ 209 { 210 c: { 211 category_symbols[c][old_idx] : category_symbols[c][new_idx] 212 for old_idx, new_idx in index_perm.items() 213 } 214 for c, index_perm in index_permutation.items() 215 } 216 for index_permutation in index_permutation_sequence 217 ] 218 219 elif selection_mode == "cohesive": 220 221 bigram_indices = [] 222 for tra in m.transitions: 223 for trb in m.transitions: 224 if tra != trb and tra.target_state_idx == trb.origin_state_idx: 225 c_a = symbol_set.symbol_categories[ m.alphabet[tra.symbol_idx] ] 226 c_b = symbol_set.symbol_categories[ m.alphabet[trb.symbol_idx] ] 227 228 # Store the category and the original index within that category 229 orig_idx_a = category_symbols[c_a].index( m.alphabet[tra.symbol_idx] ) 230 orig_idx_b = category_symbols[c_b].index( m.alphabet[trb.symbol_idx] ) 231 232 bigram_indices.append( (c_a, orig_idx_a, c_b, orig_idx_b) ) 233 234 def calculate_score(perm_state): 235 score = 0 236 for c_a, idx_a, c_b, idx_b in bigram_indices: 237 238 new_sym_a = category_symbols[c_a][ perm_state[c_a][idx_a] ] 239 new_sym_b = category_symbols[c_b][ perm_state[c_b][idx_b] ] 240 241 global_idx_a = symbol_set.symbols.index(new_sym_a) 242 global_idx_b = symbol_set.symbols.index(new_sym_b) 243 244 score += symbol_set.symbol_cohesion[global_idx_a, global_idx_b] 245 return score 246 247 permutation_sequence = [] 248 249 # Hill Climbing Loop 250 for _ in range(n_permutations-1): 251 252 # Start with a random permutation state for each category 253 current_perm = { 254 c: list(np_rng.permutation(len(category_symbols[c]))) if c in categories_to_permute else list(range(len(category_symbols[c]))) 255 for c in ordered_categories 256 } 257 258 current_score = calculate_score(current_perm) 259 improved = True 260 261 # Keep optimizing until we hit a local maximum 262 while improved: 263 improved = False 264 265 # Try swapping every possible pair in every active category 266 for c in categories_to_permute: 267 n_sym = len(category_symbols[c]) 268 for i in range(n_sym): 269 for j in range(i + 1, n_sym): 270 271 # Perform swap 272 current_perm[c][i], current_perm[c][j] = current_perm[c][j], current_perm[c][i] 273 274 new_score = calculate_score(current_perm) 275 276 if new_score > current_score: 277 current_score = new_score 278 improved = True 279 else: 280 # Revert swap if it didn't improve the score 281 current_perm[c][i], current_perm[c][j] = current_perm[c][j], current_perm[c][i] 282 283 # Translate the optimized integer lists back into the expected dictionary format 284 final_permutation = {} 285 for c in ordered_categories : 286 final_permutation[c] = { 287 category_symbols[c][old_idx] : category_symbols[c][new_idx] 288 for old_idx, new_idx in enumerate(current_perm[c]) 289 } 290 291 permutation_sequence.append(final_permutation) 292 293 for idx, permutations in enumerate( permutation_sequence ) : 294 295 m_iso = isomorphic_to_with_category_permutations( 296 m=m, 297 symbol_set=symbol_set, 298 permutations=permutations, 299 decorator=f"@{idx}" 300 ) 301 302 m_isos.append( m_iso ) 303 304 all_machines = [ m ] + m_isos 305 306 for ma in all_machines : 307 for mb in all_machines : 308 if ma != mb : 309 for j, state in enumerate( ma.states ) : 310 ma.states[ j ].add_isomorph( mb.states[ j ].name ) 311 mb.states[ j ].add_isomorph( ma.states[ j ].name ) 312 313 return all_machines