amachine.am_create.am_star_composition
1from collections import defaultdict 2 3from sympy.core.operations import str_signature 4 5from ..am_hmm import HMM 6from ..am_causal_state import CausalState 7from ..am_transition import Transition 8from ..am_id_generator import next_composition_id 9 10def star_composition( 11 exit_symbol : str, 12 enter_symbols : list[str], 13 component_residency_factor : float, 14 component_groups : dict[str, list[HMM]], 15 instances_per_component : int | dict[ str, list[int] ] = 1, 16 composition_id : str | None = None, ) -> HMM : 17 18 if component_residency_factor <= 0.0 or component_residency_factor >= 1.0 : 19 raise ValueError( f"component_residency_factor must be in (0,1), got {component_residency_factor}." ) 20 21 if isinstance( instances_per_component, int ): 22 ic_dict = {} 23 for group_id, components in component_groups.items(): 24 ic_dict[ group_id ] = [ instances_per_component ] * len(components) 25 instances_per_component = ic_dict 26 27 if composition_id is None : 28 composition_id = str(next_composition_id()) 29 30 component_pool : list[ tuple[ HMM, str, int ] ] = [] 31 for group_id, components in component_groups.items() : 32 for c_i, c in enumerate( components ) : 33 component_pool.append( ( c, group_id, c_i ) ) 34 35 total_instances = sum( [ 36 sum( counts ) for counts in instances_per_component.values() 37 ] ) 38 39 if total_instances > len( enter_symbols ) : 40 raise Exception( f"Too few enter symbols ({len(enter_symbols)}) given number of instances ({total_instances})." ) 41 42 ############################################################## 43 # Build the alphabet 44 45 alphabet = [ exit_symbol ] 46 symbol_idx_map = { exit_symbol : 0 } 47 symbol_set = { exit_symbol } 48 49 for m, _, _ in component_pool : 50 for s in m.alphabet : 51 if s not in symbol_set : 52 symbol_idx_map[ s ] = len( alphabet ) 53 alphabet.append( s ) 54 symbol_set.add( s ) 55 56 for s in enter_symbols[ :total_instances ] : 57 if s not in symbol_set : 58 symbol_idx_map[ s ] = len( alphabet ) 59 alphabet.append( s ) 60 symbol_set.add( s ) 61 62 ############################################################## 63 # Add the states and transitions 64 65 cstate_name_to_global_idxs = { 66 group_id : defaultdict( list ) 67 for group_id in component_groups.keys() 68 } 69 70 cstate_instance_to_global_idx = [ {} for _ in range( total_instances ) ] 71 global_idx_to_component_state = {} 72 global_idx_to_group = {} 73 g_sidx = 0 74 instance_idx = 0 75 states = [] 76 transitions = [] 77 78 composition_depth = max( ( 79 m.composition_depth for gr in component_groups.values() for m in gr 80 ) ) + 1 81 82 connector_state = CausalState( name=f"/c_{composition_id}" ) 83 connector_state.add_class( composition_depth, "connector" ) 84 85 states.append( connector_state ) 86 connector_g_sidx = g_sidx 87 g_sidx += 1 88 89 for m_idx, ( m, group_id, group_offset ) in enumerate( component_pool ) : 90 91 # Instance count for this component 92 n_instances = instances_per_component[ group_id ][ group_offset ] 93 94 for _ in range( n_instances ): 95 96 for s_idx, state in enumerate( m.states ) : 97 98 m_classes = { 99 composition_depth : { 100 f"cm_{composition_id}_{m_idx}", 101 f"cmi_{composition_id}_{instance_idx}", 102 f"c_{composition_id}", 103 f"g_{group_id}" 104 } 105 } 106 107 # Isomorphs are remapped in a later pass 108 states.append( CausalState( 109 name=f"{g_sidx}/{state.name}", 110 classes=( state.classes | m_classes ) 111 ) ) 112 113 global_idx_to_component_state[ g_sidx ] = state 114 global_idx_to_group[ g_sidx ] = group_id 115 cstate_name_to_global_idxs[ group_id ][ state.name ].append( g_sidx ) 116 cstate_instance_to_global_idx[ instance_idx ][ s_idx ] = g_sidx 117 118 g_sidx += 1 119 120 for tr in m.transitions : 121 122 origin = cstate_instance_to_global_idx[ instance_idx ][ tr.origin_state_idx ] 123 target = cstate_instance_to_global_idx[ instance_idx ][ tr.target_state_idx ] 124 125 transitions.append( tr.modified_deep_copy( 126 origin_state_idx=origin, 127 target_state_idx=target, 128 symbol_idx=symbol_idx_map[ m.alphabet[ tr.symbol_idx ] ] 129 ) ) 130 131 m_entry_g_sidx = cstate_instance_to_global_idx[ instance_idx ][ m.start_state ] 132 133 # all of the transitions from the entry state need to have their probabilities adjusted 134 # since we will add a new transition to the connector 135 entry_tr_ids = [ 136 i for i, tr in enumerate( transitions ) 137 if tr.origin_state_idx == m_entry_g_sidx 138 ] 139 140 for i in entry_tr_ids : 141 transitions[ i ] = transitions[ i ].modified_deep_copy( 142 prob = transitions[ i ].prob * component_residency_factor 143 ) 144 145 # Entry state -> connector (escape with remaining probability mass) 146 transitions.append( Transition( 147 origin_state_idx=m_entry_g_sidx, 148 target_state_idx=connector_g_sidx, 149 prob=( 1.0 - component_residency_factor ), 150 symbol_idx=symbol_idx_map[ exit_symbol ], 151 composition_depth=composition_depth 152 ) ) 153 154 # Connector -> entry state 155 # Probability is 1.0 / total_instances, mapping to unique enter symbol per instance 156 transitions.append( Transition( 157 origin_state_idx=connector_g_sidx, 158 target_state_idx=m_entry_g_sidx, 159 prob=( 1.0 / total_instances ), 160 symbol_idx=symbol_idx_map[ enter_symbols[ instance_idx ] ], 161 composition_depth=composition_depth 162 ) ) 163 164 # Increment global instance tracker 165 instance_idx += 1 166 167 # Remap and add the isomorphs 168 for g_idx, s in enumerate( states ) : 169 170 if g_idx not in global_idx_to_component_state : 171 continue # skip the connector state 172 173 group_id = global_idx_to_group[ g_idx ] 174 c_state = global_idx_to_component_state[ g_idx ] 175 176 isomorphs = set() 177 pseudo_isomorphs = set() 178 179 for c_iso in c_state.isomorphs : 180 # Isomorphs are scoped to the group 181 g_isos = cstate_name_to_global_idxs[ group_id ][ c_iso ] 182 for g_iso in g_isos : 183 isomorphs.add( states[ g_iso ].name ) 184 185 for c_iso in c_state.pseudo_isomorphs : 186 g_isos = cstate_name_to_global_idxs[ group_id ][ c_iso ] 187 for g_iso in g_isos : 188 pseudo_isomorphs.add( states[ g_iso ].name ) 189 190 states[ g_idx ] = states[ g_idx ].modified_deep_copy( 191 isomorphs=isomorphs, 192 pseudo_isomorphs=pseudo_isomorphs 193 ) 194 195 ############################################################## 196 197 res = HMM( 198 states=states, 199 transitions=transitions, 200 alphabet=alphabet, 201 start_state=connector_g_sidx, 202 composition_depth=composition_depth 203 ) 204 205 if not res.is_row_stochastic() : 206 raise Exception( "Start composition is not row stochastic" ) 207 208 if not res.is_unifilar() : 209 raise Exception( "Start composition is not unifilar" ) 210 211 return res
def
star_composition( exit_symbol: str, enter_symbols: list[str], component_residency_factor: float, component_groups: dict[str, list[amachine.am_hmm.HMM]], instances_per_component: int | dict[str, list[int]] = 1, composition_id: str | None = None) -> amachine.am_hmm.HMM:
11def star_composition( 12 exit_symbol : str, 13 enter_symbols : list[str], 14 component_residency_factor : float, 15 component_groups : dict[str, list[HMM]], 16 instances_per_component : int | dict[ str, list[int] ] = 1, 17 composition_id : str | None = None, ) -> HMM : 18 19 if component_residency_factor <= 0.0 or component_residency_factor >= 1.0 : 20 raise ValueError( f"component_residency_factor must be in (0,1), got {component_residency_factor}." ) 21 22 if isinstance( instances_per_component, int ): 23 ic_dict = {} 24 for group_id, components in component_groups.items(): 25 ic_dict[ group_id ] = [ instances_per_component ] * len(components) 26 instances_per_component = ic_dict 27 28 if composition_id is None : 29 composition_id = str(next_composition_id()) 30 31 component_pool : list[ tuple[ HMM, str, int ] ] = [] 32 for group_id, components in component_groups.items() : 33 for c_i, c in enumerate( components ) : 34 component_pool.append( ( c, group_id, c_i ) ) 35 36 total_instances = sum( [ 37 sum( counts ) for counts in instances_per_component.values() 38 ] ) 39 40 if total_instances > len( enter_symbols ) : 41 raise Exception( f"Too few enter symbols ({len(enter_symbols)}) given number of instances ({total_instances})." ) 42 43 ############################################################## 44 # Build the alphabet 45 46 alphabet = [ exit_symbol ] 47 symbol_idx_map = { exit_symbol : 0 } 48 symbol_set = { exit_symbol } 49 50 for m, _, _ in component_pool : 51 for s in m.alphabet : 52 if s not in symbol_set : 53 symbol_idx_map[ s ] = len( alphabet ) 54 alphabet.append( s ) 55 symbol_set.add( s ) 56 57 for s in enter_symbols[ :total_instances ] : 58 if s not in symbol_set : 59 symbol_idx_map[ s ] = len( alphabet ) 60 alphabet.append( s ) 61 symbol_set.add( s ) 62 63 ############################################################## 64 # Add the states and transitions 65 66 cstate_name_to_global_idxs = { 67 group_id : defaultdict( list ) 68 for group_id in component_groups.keys() 69 } 70 71 cstate_instance_to_global_idx = [ {} for _ in range( total_instances ) ] 72 global_idx_to_component_state = {} 73 global_idx_to_group = {} 74 g_sidx = 0 75 instance_idx = 0 76 states = [] 77 transitions = [] 78 79 composition_depth = max( ( 80 m.composition_depth for gr in component_groups.values() for m in gr 81 ) ) + 1 82 83 connector_state = CausalState( name=f"/c_{composition_id}" ) 84 connector_state.add_class( composition_depth, "connector" ) 85 86 states.append( connector_state ) 87 connector_g_sidx = g_sidx 88 g_sidx += 1 89 90 for m_idx, ( m, group_id, group_offset ) in enumerate( component_pool ) : 91 92 # Instance count for this component 93 n_instances = instances_per_component[ group_id ][ group_offset ] 94 95 for _ in range( n_instances ): 96 97 for s_idx, state in enumerate( m.states ) : 98 99 m_classes = { 100 composition_depth : { 101 f"cm_{composition_id}_{m_idx}", 102 f"cmi_{composition_id}_{instance_idx}", 103 f"c_{composition_id}", 104 f"g_{group_id}" 105 } 106 } 107 108 # Isomorphs are remapped in a later pass 109 states.append( CausalState( 110 name=f"{g_sidx}/{state.name}", 111 classes=( state.classes | m_classes ) 112 ) ) 113 114 global_idx_to_component_state[ g_sidx ] = state 115 global_idx_to_group[ g_sidx ] = group_id 116 cstate_name_to_global_idxs[ group_id ][ state.name ].append( g_sidx ) 117 cstate_instance_to_global_idx[ instance_idx ][ s_idx ] = g_sidx 118 119 g_sidx += 1 120 121 for tr in m.transitions : 122 123 origin = cstate_instance_to_global_idx[ instance_idx ][ tr.origin_state_idx ] 124 target = cstate_instance_to_global_idx[ instance_idx ][ tr.target_state_idx ] 125 126 transitions.append( tr.modified_deep_copy( 127 origin_state_idx=origin, 128 target_state_idx=target, 129 symbol_idx=symbol_idx_map[ m.alphabet[ tr.symbol_idx ] ] 130 ) ) 131 132 m_entry_g_sidx = cstate_instance_to_global_idx[ instance_idx ][ m.start_state ] 133 134 # all of the transitions from the entry state need to have their probabilities adjusted 135 # since we will add a new transition to the connector 136 entry_tr_ids = [ 137 i for i, tr in enumerate( transitions ) 138 if tr.origin_state_idx == m_entry_g_sidx 139 ] 140 141 for i in entry_tr_ids : 142 transitions[ i ] = transitions[ i ].modified_deep_copy( 143 prob = transitions[ i ].prob * component_residency_factor 144 ) 145 146 # Entry state -> connector (escape with remaining probability mass) 147 transitions.append( Transition( 148 origin_state_idx=m_entry_g_sidx, 149 target_state_idx=connector_g_sidx, 150 prob=( 1.0 - component_residency_factor ), 151 symbol_idx=symbol_idx_map[ exit_symbol ], 152 composition_depth=composition_depth 153 ) ) 154 155 # Connector -> entry state 156 # Probability is 1.0 / total_instances, mapping to unique enter symbol per instance 157 transitions.append( Transition( 158 origin_state_idx=connector_g_sidx, 159 target_state_idx=m_entry_g_sidx, 160 prob=( 1.0 / total_instances ), 161 symbol_idx=symbol_idx_map[ enter_symbols[ instance_idx ] ], 162 composition_depth=composition_depth 163 ) ) 164 165 # Increment global instance tracker 166 instance_idx += 1 167 168 # Remap and add the isomorphs 169 for g_idx, s in enumerate( states ) : 170 171 if g_idx not in global_idx_to_component_state : 172 continue # skip the connector state 173 174 group_id = global_idx_to_group[ g_idx ] 175 c_state = global_idx_to_component_state[ g_idx ] 176 177 isomorphs = set() 178 pseudo_isomorphs = set() 179 180 for c_iso in c_state.isomorphs : 181 # Isomorphs are scoped to the group 182 g_isos = cstate_name_to_global_idxs[ group_id ][ c_iso ] 183 for g_iso in g_isos : 184 isomorphs.add( states[ g_iso ].name ) 185 186 for c_iso in c_state.pseudo_isomorphs : 187 g_isos = cstate_name_to_global_idxs[ group_id ][ c_iso ] 188 for g_iso in g_isos : 189 pseudo_isomorphs.add( states[ g_iso ].name ) 190 191 states[ g_idx ] = states[ g_idx ].modified_deep_copy( 192 isomorphs=isomorphs, 193 pseudo_isomorphs=pseudo_isomorphs 194 ) 195 196 ############################################################## 197 198 res = HMM( 199 states=states, 200 transitions=transitions, 201 alphabet=alphabet, 202 start_state=connector_g_sidx, 203 composition_depth=composition_depth 204 ) 205 206 if not res.is_row_stochastic() : 207 raise Exception( "Start composition is not row stochastic" ) 208 209 if not res.is_unifilar() : 210 raise Exception( "Start composition is not unifilar" ) 211 212 return res