GitLab Repo

amachine.am_create.am_structured_isomorphic_rotation

  1from collections import defaultdict
  2import copy
  3import random
  4from typing import Literal
  5import warnings 
  6import math
  7
  8import numpy as np
  9
 10from ..am_hmm          import HMM
 11from ..am_causal_state import CausalState
 12from ..am_transition   import Transition
 13
 14from ..am_random import exp_uniform_blend, resolve_rng
 15from ..am_vocabulary import Vocabulary
 16from ..am_structured_symbol_set import StructuredSymbolSet
 17
 18from .am_random_structured import random_structured
 19from .am_isomorphic_to_with_category_rotation import isomorphic_to_with_category_rotation
 20from .am_star_join import star_join
 21
 22def get_unique_random_combinations(
 23    category_sizes,
 24    n_samples: int,
 25    rng: np.random.Generator,
 26) -> np.ndarray:
 27
 28    sizes = tuple(map(int, category_sizes))
 29    total = math.prod(sizes)
 30
 31    if n_samples > total:
 32        raise ValueError(
 33            f"Cannot draw {n_samples} unique samples: "
 34            f"only {total} combinations exist."
 35        )
 36
 37    if total < 2**63:
 38        flat = rng.choice( range( 1, total ), size=n_samples, replace=False )
 39        
 40        return np.stack(np.unravel_index(flat, shape=sizes), axis=1).tolist()
 41
 42    else:
 43        seen: set[tuple] = set()
 44        rows: list[tuple] = []
 45        while len(rows) < n_samples:
 46            combo = tuple(int(rng.integers(0, s)) for s in sizes)
 47            if combo not in seen:
 48                seen.add(combo)
 49                rows.append(combo)
 50
 51        return np.array(rows, dtype=np.intp).tolist()
 52
 53def structured_isomorphic_rotation(
 54    isoclass_name : int,
 55    n_states : int,
 56    symbol_set : StructuredSymbolSet,
 57    connectedness : float,
 58    randomness : float,
 59    star_joined : bool,
 60    n_rotations : int,
 61    rotation_mode : Literal[ "sequential", "cohesive", "random" ] = "sequential",
 62    mode_residency_factor : float | None = None,
 63    exclude_categories : set[int] | None = None,
 64    max_search_time : float = 120.0,
 65    n_search_workers = 3,
 66    random_seed : int | None = None
 67) -> HMM | list[HMM]:
 68
 69    if exclude_categories is None :
 70        exclude_categories = set()
 71
 72    if star_joined and mode_residency_factor is None :
 73        raise ValueError( "star_join requires mode_residency factor" )
 74
 75    if rotation_mode not in { "sequential", "cohesive", "random"  } :
 76        raise ValueError( "Invalid rotation mode" )
 77
 78    if random_seed is not None:
 79        py_rng = random.Random(random_seed)
 80        np_rng = np.random.default_rng(random_seed)
 81    else:
 82        py_rng = random
 83        np_rng = resolve_rng(None)
 84
 85    m = random_structured(
 86        n_states=n_states,
 87        symbol_set=symbol_set,
 88        connectedness=connectedness,
 89        randomness=randomness,
 90        ensure_strongly_connected=True,
 91        ensure_minimal=True,
 92        max_search_time=max_search_time,
 93        n_search_workers=n_search_workers,
 94        random_seed=random_seed
 95    )
 96
 97    # Collapse to the largest recurrent subgraph
 98    m.collapse_to_largest_strongly_connected_subgraph()
 99
100    # Minimize the machine -> epsilon-machine.
101    m.minimize()
102
103    categories_to_rotate = symbol_set.categories - exclude_categories
104
105    category_sizes = symbol_set.symbol_counts_by_category()
106    n_categories = len( category_sizes )
107
108    m.isoclass = isoclass_name
109    full_orbit_rotations = math.lcm( *( category_sizes[ i ] for i in categories_to_rotate ) ) - 1
110
111    print( f"Creating {n_rotations+1} total isomorphic HMMs" )
112
113    m_isos = []
114
115    enter_symbol_pool = list( set( 
116        Vocabulary.digits()
117      + Vocabulary.letters_lower()
118      + Vocabulary.letters_upper()
119      + Vocabulary.greek_lower()
120      + Vocabulary.greek_upper() ) - set( m.alphabet ) )
121
122    if star_joined and len( enter_symbol_pool ) < n_rotations + 1 :
123        raise ValueError( "Not enough symbols left over to construct star join." )
124
125    effective_sizes = [ 
126        category_sizes[i] if i not in exclude_categories else 1 
127        for i in range(n_categories) 
128    ]
129
130    if rotation_mode == "sequential" :
131
132        shift_sequence = [ 
133            { 
134                c : i if c in categories_to_rotate else 0
135                for c in range( n_categories ) 
136            }
137            for i in range( 1, n_rotations+1 )
138        ]
139    
140    elif rotation_mode == "random": 
141
142        combinations = get_unique_random_combinations( 
143            category_sizes=effective_sizes,
144            n_samples=n_rotations,
145            rng=np_rng
146        )
147
148        shift_sequence = [
149            {  c : p[c] for c in range(n_categories) }
150            for p in combinations
151        ]
152
153    elif rotation_mode == "cohesive" :
154
155        pool_size = min( math.prod( effective_sizes)-1, 1_000_000 )
156
157        pool = get_unique_random_combinations( 
158            category_sizes=effective_sizes,
159            n_samples=pool_size,
160            rng=np_rng
161        )
162
163        bigrams = []
164        for tra in  m.transitions :
165            for trb in m.transitions :
166                if tra == trb :
167                    continue
168                if tra.target_state_idx == trb.origin_state_idx :
169                    bigrams.append( ( 
170                        m.alphabet[ tra.symbol_idx ], 
171                        m.alphabet[ trb.symbol_idx ] ) )
172            
173
174        s_cats = {}
175        for c in set( symbol_set.symbol_categories.values() ) :
176            s_cats[ c ] = sorted( [ 
177                y for y in m.alphabet 
178                if y in symbol_set.symbol_categories and symbol_set.symbol_categories[y] == c 
179            ] )
180
181        scored_combinations = []
182        for combination in pool :
183            score = 0
184            for a, b in bigrams :
185
186                c_a = symbol_set.symbol_categories[ a ]
187                c_b = symbol_set.symbol_categories[ b ]
188
189                cat_a = s_cats[ c_a ]
190                cat_b = s_cats[ c_b ]
191
192                new_a = cat_a[ ( cat_a.index( a ) + combination[ c_a ] ) % len( cat_a ) ]
193                new_b = cat_b[ ( cat_b.index( b ) + combination[ c_b ] ) % len( cat_b ) ]
194
195                score += symbol_set.symbol_cohesion[ 
196                    symbol_set.symbols.index( new_a ), 
197                    symbol_set.symbols.index( new_b ) 
198                ]
199                
200            scored_combinations.append( ( score, combination ) )
201    
202        scored_combinations = sorted( scored_combinations, reverse=True )
203        scored_combinations = scored_combinations[ 0 : n_rotations ]
204
205        shift_sequence = [
206            {  c : p[c] for c in range(n_categories) }
207            for score, p in scored_combinations
208        ]
209
210    for categories_shifts in shift_sequence :
211
212        cs = sorted( [ item for item in categories_shifts.items() ] )
213        cs = [ str(x[1]) for x in cs ]
214
215        m_iso = isomorphic_to_with_category_rotation(
216            m=m,
217            symbol_categories=symbol_set.symbol_categories,
218            categories_shifts=categories_shifts,
219            decorator=f"@{"-".join(cs)}" 
220        )
221
222        m_iso.isoclass = isoclass_name
223        m_isos.append( m_iso )
224
225    all_machines = [ m ] + m_isos
226
227    for ma in all_machines :
228        for mb in all_machines :
229            if ma != mb :
230                for j, state in enumerate( ma.states ) : 
231                    ma.states[ j ].add_isomorph( mb.states[ j ].name )
232                    mb.states[ j ].add_isomorph( ma.states[ j ].name )
233
234    if star_joined :
235
236        # Join them together
237        m_star = star_join(
238            exit_symbol='x', 
239            enter_symbols=enter_symbol_pool[ 0:len(all_machines) ],
240            machines=all_machines,
241            mode_residency_factor=mode_residency_factor 
242        )
243
244        return m_star
245    
246    else :
247        return all_machines
def get_unique_random_combinations( category_sizes, n_samples: int, rng: numpy.random._generator.Generator) -> numpy.ndarray:
23def get_unique_random_combinations(
24    category_sizes,
25    n_samples: int,
26    rng: np.random.Generator,
27) -> np.ndarray:
28
29    sizes = tuple(map(int, category_sizes))
30    total = math.prod(sizes)
31
32    if n_samples > total:
33        raise ValueError(
34            f"Cannot draw {n_samples} unique samples: "
35            f"only {total} combinations exist."
36        )
37
38    if total < 2**63:
39        flat = rng.choice( range( 1, total ), size=n_samples, replace=False )
40        
41        return np.stack(np.unravel_index(flat, shape=sizes), axis=1).tolist()
42
43    else:
44        seen: set[tuple] = set()
45        rows: list[tuple] = []
46        while len(rows) < n_samples:
47            combo = tuple(int(rng.integers(0, s)) for s in sizes)
48            if combo not in seen:
49                seen.add(combo)
50                rows.append(combo)
51
52        return np.array(rows, dtype=np.intp).tolist()
def structured_isomorphic_rotation( isoclass_name: int, n_states: int, symbol_set: amachine.am_structured_symbol_set.StructuredSymbolSet, connectedness: float, randomness: float, star_joined: bool, n_rotations: int, rotation_mode: Literal['sequential', 'cohesive', 'random'] = 'sequential', mode_residency_factor: float | None = None, exclude_categories: set[int] | None = None, max_search_time: float = 120.0, n_search_workers=3, random_seed: int | None = None) -> amachine.am_hmm.HMM | list[amachine.am_hmm.HMM]:
 54def structured_isomorphic_rotation(
 55    isoclass_name : int,
 56    n_states : int,
 57    symbol_set : StructuredSymbolSet,
 58    connectedness : float,
 59    randomness : float,
 60    star_joined : bool,
 61    n_rotations : int,
 62    rotation_mode : Literal[ "sequential", "cohesive", "random" ] = "sequential",
 63    mode_residency_factor : float | None = None,
 64    exclude_categories : set[int] | None = None,
 65    max_search_time : float = 120.0,
 66    n_search_workers = 3,
 67    random_seed : int | None = None
 68) -> HMM | list[HMM]:
 69
 70    if exclude_categories is None :
 71        exclude_categories = set()
 72
 73    if star_joined and mode_residency_factor is None :
 74        raise ValueError( "star_join requires mode_residency factor" )
 75
 76    if rotation_mode not in { "sequential", "cohesive", "random"  } :
 77        raise ValueError( "Invalid rotation mode" )
 78
 79    if random_seed is not None:
 80        py_rng = random.Random(random_seed)
 81        np_rng = np.random.default_rng(random_seed)
 82    else:
 83        py_rng = random
 84        np_rng = resolve_rng(None)
 85
 86    m = random_structured(
 87        n_states=n_states,
 88        symbol_set=symbol_set,
 89        connectedness=connectedness,
 90        randomness=randomness,
 91        ensure_strongly_connected=True,
 92        ensure_minimal=True,
 93        max_search_time=max_search_time,
 94        n_search_workers=n_search_workers,
 95        random_seed=random_seed
 96    )
 97
 98    # Collapse to the largest recurrent subgraph
 99    m.collapse_to_largest_strongly_connected_subgraph()
100
101    # Minimize the machine -> epsilon-machine.
102    m.minimize()
103
104    categories_to_rotate = symbol_set.categories - exclude_categories
105
106    category_sizes = symbol_set.symbol_counts_by_category()
107    n_categories = len( category_sizes )
108
109    m.isoclass = isoclass_name
110    full_orbit_rotations = math.lcm( *( category_sizes[ i ] for i in categories_to_rotate ) ) - 1
111
112    print( f"Creating {n_rotations+1} total isomorphic HMMs" )
113
114    m_isos = []
115
116    enter_symbol_pool = list( set( 
117        Vocabulary.digits()
118      + Vocabulary.letters_lower()
119      + Vocabulary.letters_upper()
120      + Vocabulary.greek_lower()
121      + Vocabulary.greek_upper() ) - set( m.alphabet ) )
122
123    if star_joined and len( enter_symbol_pool ) < n_rotations + 1 :
124        raise ValueError( "Not enough symbols left over to construct star join." )
125
126    effective_sizes = [ 
127        category_sizes[i] if i not in exclude_categories else 1 
128        for i in range(n_categories) 
129    ]
130
131    if rotation_mode == "sequential" :
132
133        shift_sequence = [ 
134            { 
135                c : i if c in categories_to_rotate else 0
136                for c in range( n_categories ) 
137            }
138            for i in range( 1, n_rotations+1 )
139        ]
140    
141    elif rotation_mode == "random": 
142
143        combinations = get_unique_random_combinations( 
144            category_sizes=effective_sizes,
145            n_samples=n_rotations,
146            rng=np_rng
147        )
148
149        shift_sequence = [
150            {  c : p[c] for c in range(n_categories) }
151            for p in combinations
152        ]
153
154    elif rotation_mode == "cohesive" :
155
156        pool_size = min( math.prod( effective_sizes)-1, 1_000_000 )
157
158        pool = get_unique_random_combinations( 
159            category_sizes=effective_sizes,
160            n_samples=pool_size,
161            rng=np_rng
162        )
163
164        bigrams = []
165        for tra in  m.transitions :
166            for trb in m.transitions :
167                if tra == trb :
168                    continue
169                if tra.target_state_idx == trb.origin_state_idx :
170                    bigrams.append( ( 
171                        m.alphabet[ tra.symbol_idx ], 
172                        m.alphabet[ trb.symbol_idx ] ) )
173            
174
175        s_cats = {}
176        for c in set( symbol_set.symbol_categories.values() ) :
177            s_cats[ c ] = sorted( [ 
178                y for y in m.alphabet 
179                if y in symbol_set.symbol_categories and symbol_set.symbol_categories[y] == c 
180            ] )
181
182        scored_combinations = []
183        for combination in pool :
184            score = 0
185            for a, b in bigrams :
186
187                c_a = symbol_set.symbol_categories[ a ]
188                c_b = symbol_set.symbol_categories[ b ]
189
190                cat_a = s_cats[ c_a ]
191                cat_b = s_cats[ c_b ]
192
193                new_a = cat_a[ ( cat_a.index( a ) + combination[ c_a ] ) % len( cat_a ) ]
194                new_b = cat_b[ ( cat_b.index( b ) + combination[ c_b ] ) % len( cat_b ) ]
195
196                score += symbol_set.symbol_cohesion[ 
197                    symbol_set.symbols.index( new_a ), 
198                    symbol_set.symbols.index( new_b ) 
199                ]
200                
201            scored_combinations.append( ( score, combination ) )
202    
203        scored_combinations = sorted( scored_combinations, reverse=True )
204        scored_combinations = scored_combinations[ 0 : n_rotations ]
205
206        shift_sequence = [
207            {  c : p[c] for c in range(n_categories) }
208            for score, p in scored_combinations
209        ]
210
211    for categories_shifts in shift_sequence :
212
213        cs = sorted( [ item for item in categories_shifts.items() ] )
214        cs = [ str(x[1]) for x in cs ]
215
216        m_iso = isomorphic_to_with_category_rotation(
217            m=m,
218            symbol_categories=symbol_set.symbol_categories,
219            categories_shifts=categories_shifts,
220            decorator=f"@{"-".join(cs)}" 
221        )
222
223        m_iso.isoclass = isoclass_name
224        m_isos.append( m_iso )
225
226    all_machines = [ m ] + m_isos
227
228    for ma in all_machines :
229        for mb in all_machines :
230            if ma != mb :
231                for j, state in enumerate( ma.states ) : 
232                    ma.states[ j ].add_isomorph( mb.states[ j ].name )
233                    mb.states[ j ].add_isomorph( ma.states[ j ].name )
234
235    if star_joined :
236
237        # Join them together
238        m_star = star_join(
239            exit_symbol='x', 
240            enter_symbols=enter_symbol_pool[ 0:len(all_machines) ],
241            machines=all_machines,
242            mode_residency_factor=mode_residency_factor 
243        )
244
245        return m_star
246    
247    else :
248        return all_machines