GitLab Repo

amachine.am_structured_symbol_set

  1from dataclasses import dataclass
  2from collections import Counter
  3import math
  4import json
  5import numpy as np
  6
  7from .am_random import exp_uniform_blend, resolve_rng
  8from .am_vocabulary import Vocabulary
  9
 10@dataclass
 11class StructuredSymbolSet :
 12
 13    categories : set[int]
 14    category_cohesion : np.ndarray # 2d
 15    symbols : list[str]
 16    symbol_categories : dict[str,int]
 17    symbol_cohesion : np.ndarray # 2d
 18
 19    def save_json( 
 20        self, 
 21        filename: str, 
 22        eoc_symbols : list[set[str]] | None = None ) -> None :
 23
 24        if eoc_symbols is None :
 25            eoc_symbols = []
 26
 27        data = {
 28            "categories" : sorted(self.categories),
 29            "category_cohesion" : self.category_cohesion.tolist(),
 30            "symbols" : self.symbols,
 31            "symbol_categories" : self.symbol_categories,
 32            "symbol_cohesion" : self.symbol_cohesion.tolist(),
 33            "eoc_symbols" : [ sorted( syms ) for syms in eoc_symbols ]
 34        }
 35        
 36        with open(filename, 'w') as f:
 37            json.dump(data, f, indent=4)
 38
 39    def symbol_counts_by_category( self ) -> dict[int,int] : 
 40        return dict(Counter(self.symbol_categories.values()))
 41
 42    def cyclic_orbit_size(self, invariant_categories: set[int] | None = None) -> int: 
 43        
 44        if invariant_categories is None:
 45            invariant_categories = set()
 46
 47        category_sizes = self.symbol_counts_by_category()
 48
 49        effective_sizes = [
 50            size for category, size in category_sizes.items() 
 51            if category not in invariant_categories
 52        ]
 53
 54        if not effective_sizes:
 55            return 1
 56            
 57        return math.lcm(*effective_sizes)
 58
 59    def category_symbols( self ) -> dict[ int,list[str] ] : 
 60        res = {}
 61        for c in self.categories :
 62            res[ c ] = sorted( [ 
 63                s for s in self.symbols if self.symbol_categories[ s ] == c 
 64            ] )
 65        return res
 66
 67    @classmethod
 68    def generate( 
 69        cls, 
 70        n_categories                : int,
 71        symbols_per_category        : int | list[int],
 72        rigidity                    : float,
 73        within_category_variability : float,
 74        category_repetition_penalty : float = 0.5,
 75        symbol_repetition_penalty   : float = 0.85,
 76        np_rng : np.random.Generator | None = None ) -> "StructuredSymbolSet" :
 77
 78        if isinstance( symbols_per_category, int ) :
 79            symbols_per_category = [symbols_per_category]*n_categories
 80
 81        elif len( symbols_per_category ) != n_categories :
 82            raise ValueError( "len( symbols_per_category ) != n_categories" )
 83
 84        if within_category_variability < 0.0 or within_category_variability > 1.0 :
 85            raise ValueError( f"within_category_variability should range from 0 to 1" )
 86
 87        if category_repetition_penalty < 0.0 or category_repetition_penalty > 1.0 :
 88            raise ValueError( f"category_repetition_penalty should range from 0 to 1" )
 89
 90        if symbol_repetition_penalty < 0.0 or symbol_repetition_penalty > 1.0 :
 91            raise ValueError( f"symbol_repetition_penalty should range from 0 to 1" )
 92
 93        rng = resolve_rng( np_rng )
 94
 95        n_symbols = sum( count for count in symbols_per_category )
 96
 97        symbol_pool = Vocabulary.digits() + Vocabulary.letters_lower() + Vocabulary.greek_lower() + Vocabulary.greek_upper()
 98
 99        if len( symbol_pool ) < n_symbols :
100            raise ValueError( f"Too many symbols required, n_categories*symbols_per_category must be < {len(symbol_pool)}." )
101
102        symbol_categories = {}
103        alphabet = []
104        s_idx = 0
105
106        for i in range( n_categories ) :
107            for j in range( symbols_per_category[ i ] ) :
108                alphabet.append( symbol_pool[ s_idx ] )
109                symbol_categories[ symbol_pool[ s_idx ] ] = i
110                s_idx += 1
111
112        category_cohesion = np.zeros( ( n_categories, n_categories ) )
113        for i in range( n_categories ) :
114            
115            category_cohesion[ i, : ] = exp_uniform_blend( 
116                n=n_categories, 
117                alpha=(1.0-rigidity),
118                np_rng=rng )
119
120            category_cohesion[ i, i ] *= (1.0-category_repetition_penalty)
121
122        symbol_cohesion = np.zeros( ( n_symbols, n_symbols ) )
123        for i, symb_a in enumerate( alphabet ) :
124            for j, symb_b in enumerate( alphabet ) :
125                
126                c_a = symbol_categories[ symb_a ]
127                c_b = symbol_categories[ symb_b ]
128                
129                symbol_cohesion[ i, j ] = category_cohesion[ c_a, c_b ] + category_cohesion[ c_a, c_b ] * rng.random() * within_category_variability
130
131                if i == j :
132                    symbol_cohesion[ i, j ] *= ( 1.0 - symbol_repetition_penalty )
133
134        return cls(
135            categories=set( symbol_categories.values() ),
136            category_cohesion=category_cohesion,
137            symbols=alphabet,
138            symbol_categories=symbol_categories,
139            symbol_cohesion=symbol_cohesion
140        )
141
142    @classmethod
143    def generate_principled( 
144        cls, 
145        n_categories                      : int,
146        min_category_size                 : int,
147        category_growth_rate              : float,
148        small_category_bias               : float,
149        within_category_variability       : float,
150        category_repetition_penalty       : float = 0.5,
151        symbol_repetition_penalty         : float = 0.85,
152        np_rng : np.random.Generator      | None = None ) -> "StructuredSymbolSet" :
153
154        if small_category_bias < 0.0 or small_category_bias > 1.0 :
155            raise ValueError( f"small_category_bias should range from 0 to 1" )
156
157        if within_category_variability < 0.0 or within_category_variability > 1.0 :
158            raise ValueError( f"within_category_variability should range from 0 to 1" )
159
160        if category_repetition_penalty < 0.0 or category_repetition_penalty > 1.0 :
161            raise ValueError( f"category_repetition_penalty should range from 0 to 1" )
162
163        if symbol_repetition_penalty < 0.0 or symbol_repetition_penalty > 1.0 :
164            raise ValueError( f"symbol_repetition_penalty should range from 0 to 1" )
165
166        rng = resolve_rng( np_rng )
167
168        category_sizes = [ min_category_size ]
169        last_term = min_category_size
170
171        for c in range( n_categories - 1 ) : 
172            next_term = last_term * category_growth_rate
173            category_sizes.append( int( round( next_term ) ) )
174            last_term = next_term
175
176        n_symbols   = sum( category_sizes )
177        symbol_pool = Vocabulary.digits() + Vocabulary.letters_lower() + Vocabulary.greek_lower() + Vocabulary.greek_upper()
178
179        if len( symbol_pool ) < n_symbols :
180            raise ValueError( f"Not enough symbols available. {n_symbols} requested, but pool has {len(symbol_pool)}." )
181
182        symbol_categories = {}
183        alphabet = []
184        s_idx = 0
185
186        for i in range( n_categories ) :
187            for j in range( category_sizes[ i ] ) :
188                alphabet.append( symbol_pool[ s_idx ] )
189                symbol_categories[ symbol_pool[ s_idx ] ] = i
190                s_idx += 1
191
192        min_size = min( category_sizes )
193        max_size = max( category_sizes )
194
195        max_size_difference = max_size - min_size 
196        uniform = 1.0 / n_categories
197
198        category_cohesion = np.zeros( ( n_categories, n_categories ) )
199
200        if max_size_difference == 0 :
201            category_cohesion[ :, : ] = uniform
202
203        else :
204
205            # maximum small category bias
206            biased = min_category_size / np.array(category_sizes, dtype=float)
207
208            # apply bias factor
209            biased = small_category_bias * biased + ( 1.0 - small_category_bias ) * uniform
210
211            # normalized
212            biased = biased / np.sum( biased )
213
214            for i in range( n_categories ) :
215
216                # Small category is more uniformly selective, 
217                # Larger categories biased towareds selecting smaller categories
218                # r_i = ( category_sizes[i] - min_size ) / ( max_size - min_size )
219
220                r_i = 1.0 - 1.0 / math.pow( category_sizes[i], 0.5 )
221
222                # Intentionally not vectorized for readability
223                for j in range( n_categories ) :
224                    category_cohesion[ i, j ] = r_i * biased[ j ] + ( 1.0 - r_i ) * uniform
225
226                # repetition
227                category_cohesion[ i, i ] *= ( 1.0 - category_repetition_penalty )
228            
229                # normalize row
230                row_sum = np.sum( category_cohesion[ i, : ] )
231                if row_sum > 0 :
232                    category_cohesion[ i, : ] = category_cohesion[ i, : ] / row_sum
233        
234        symbol_cohesion = np.zeros( ( n_symbols, n_symbols ) )
235        for i, symb_a in enumerate( alphabet ) :
236            
237            c_a = symbol_categories[ symb_a ]
238            
239            for j, symb_b in enumerate( alphabet ) :
240                
241                c_b = symbol_categories[ symb_b ]
242                
243                symbol_cohesion[ i, j ] = category_cohesion[ c_a, c_b ] + category_cohesion[ c_a, c_b ] * rng.random() * within_category_variability
244
245                if i == j :
246                    symbol_cohesion[ i, j ] *= ( 1.0 - symbol_repetition_penalty )
247
248            row_sum = np.sum( symbol_cohesion[i, : ] )
249            if row_sum > 0 :
250                symbol_cohesion[i, : ] = symbol_cohesion[i, : ] / row_sum
251
252        return cls(
253            categories=set( symbol_categories.values() ),
254            category_cohesion=category_cohesion,
255            symbols=alphabet,
256            symbol_categories=symbol_categories,
257            symbol_cohesion=symbol_cohesion
258        )
@dataclass
class StructuredSymbolSet:
 11@dataclass
 12class StructuredSymbolSet :
 13
 14    categories : set[int]
 15    category_cohesion : np.ndarray # 2d
 16    symbols : list[str]
 17    symbol_categories : dict[str,int]
 18    symbol_cohesion : np.ndarray # 2d
 19
 20    def save_json( 
 21        self, 
 22        filename: str, 
 23        eoc_symbols : list[set[str]] | None = None ) -> None :
 24
 25        if eoc_symbols is None :
 26            eoc_symbols = []
 27
 28        data = {
 29            "categories" : sorted(self.categories),
 30            "category_cohesion" : self.category_cohesion.tolist(),
 31            "symbols" : self.symbols,
 32            "symbol_categories" : self.symbol_categories,
 33            "symbol_cohesion" : self.symbol_cohesion.tolist(),
 34            "eoc_symbols" : [ sorted( syms ) for syms in eoc_symbols ]
 35        }
 36        
 37        with open(filename, 'w') as f:
 38            json.dump(data, f, indent=4)
 39
 40    def symbol_counts_by_category( self ) -> dict[int,int] : 
 41        return dict(Counter(self.symbol_categories.values()))
 42
 43    def cyclic_orbit_size(self, invariant_categories: set[int] | None = None) -> int: 
 44        
 45        if invariant_categories is None:
 46            invariant_categories = set()
 47
 48        category_sizes = self.symbol_counts_by_category()
 49
 50        effective_sizes = [
 51            size for category, size in category_sizes.items() 
 52            if category not in invariant_categories
 53        ]
 54
 55        if not effective_sizes:
 56            return 1
 57            
 58        return math.lcm(*effective_sizes)
 59
 60    def category_symbols( self ) -> dict[ int,list[str] ] : 
 61        res = {}
 62        for c in self.categories :
 63            res[ c ] = sorted( [ 
 64                s for s in self.symbols if self.symbol_categories[ s ] == c 
 65            ] )
 66        return res
 67
 68    @classmethod
 69    def generate( 
 70        cls, 
 71        n_categories                : int,
 72        symbols_per_category        : int | list[int],
 73        rigidity                    : float,
 74        within_category_variability : float,
 75        category_repetition_penalty : float = 0.5,
 76        symbol_repetition_penalty   : float = 0.85,
 77        np_rng : np.random.Generator | None = None ) -> "StructuredSymbolSet" :
 78
 79        if isinstance( symbols_per_category, int ) :
 80            symbols_per_category = [symbols_per_category]*n_categories
 81
 82        elif len( symbols_per_category ) != n_categories :
 83            raise ValueError( "len( symbols_per_category ) != n_categories" )
 84
 85        if within_category_variability < 0.0 or within_category_variability > 1.0 :
 86            raise ValueError( f"within_category_variability should range from 0 to 1" )
 87
 88        if category_repetition_penalty < 0.0 or category_repetition_penalty > 1.0 :
 89            raise ValueError( f"category_repetition_penalty should range from 0 to 1" )
 90
 91        if symbol_repetition_penalty < 0.0 or symbol_repetition_penalty > 1.0 :
 92            raise ValueError( f"symbol_repetition_penalty should range from 0 to 1" )
 93
 94        rng = resolve_rng( np_rng )
 95
 96        n_symbols = sum( count for count in symbols_per_category )
 97
 98        symbol_pool = Vocabulary.digits() + Vocabulary.letters_lower() + Vocabulary.greek_lower() + Vocabulary.greek_upper()
 99
100        if len( symbol_pool ) < n_symbols :
101            raise ValueError( f"Too many symbols required, n_categories*symbols_per_category must be < {len(symbol_pool)}." )
102
103        symbol_categories = {}
104        alphabet = []
105        s_idx = 0
106
107        for i in range( n_categories ) :
108            for j in range( symbols_per_category[ i ] ) :
109                alphabet.append( symbol_pool[ s_idx ] )
110                symbol_categories[ symbol_pool[ s_idx ] ] = i
111                s_idx += 1
112
113        category_cohesion = np.zeros( ( n_categories, n_categories ) )
114        for i in range( n_categories ) :
115            
116            category_cohesion[ i, : ] = exp_uniform_blend( 
117                n=n_categories, 
118                alpha=(1.0-rigidity),
119                np_rng=rng )
120
121            category_cohesion[ i, i ] *= (1.0-category_repetition_penalty)
122
123        symbol_cohesion = np.zeros( ( n_symbols, n_symbols ) )
124        for i, symb_a in enumerate( alphabet ) :
125            for j, symb_b in enumerate( alphabet ) :
126                
127                c_a = symbol_categories[ symb_a ]
128                c_b = symbol_categories[ symb_b ]
129                
130                symbol_cohesion[ i, j ] = category_cohesion[ c_a, c_b ] + category_cohesion[ c_a, c_b ] * rng.random() * within_category_variability
131
132                if i == j :
133                    symbol_cohesion[ i, j ] *= ( 1.0 - symbol_repetition_penalty )
134
135        return cls(
136            categories=set( symbol_categories.values() ),
137            category_cohesion=category_cohesion,
138            symbols=alphabet,
139            symbol_categories=symbol_categories,
140            symbol_cohesion=symbol_cohesion
141        )
142
143    @classmethod
144    def generate_principled( 
145        cls, 
146        n_categories                      : int,
147        min_category_size                 : int,
148        category_growth_rate              : float,
149        small_category_bias               : float,
150        within_category_variability       : float,
151        category_repetition_penalty       : float = 0.5,
152        symbol_repetition_penalty         : float = 0.85,
153        np_rng : np.random.Generator      | None = None ) -> "StructuredSymbolSet" :
154
155        if small_category_bias < 0.0 or small_category_bias > 1.0 :
156            raise ValueError( f"small_category_bias should range from 0 to 1" )
157
158        if within_category_variability < 0.0 or within_category_variability > 1.0 :
159            raise ValueError( f"within_category_variability should range from 0 to 1" )
160
161        if category_repetition_penalty < 0.0 or category_repetition_penalty > 1.0 :
162            raise ValueError( f"category_repetition_penalty should range from 0 to 1" )
163
164        if symbol_repetition_penalty < 0.0 or symbol_repetition_penalty > 1.0 :
165            raise ValueError( f"symbol_repetition_penalty should range from 0 to 1" )
166
167        rng = resolve_rng( np_rng )
168
169        category_sizes = [ min_category_size ]
170        last_term = min_category_size
171
172        for c in range( n_categories - 1 ) : 
173            next_term = last_term * category_growth_rate
174            category_sizes.append( int( round( next_term ) ) )
175            last_term = next_term
176
177        n_symbols   = sum( category_sizes )
178        symbol_pool = Vocabulary.digits() + Vocabulary.letters_lower() + Vocabulary.greek_lower() + Vocabulary.greek_upper()
179
180        if len( symbol_pool ) < n_symbols :
181            raise ValueError( f"Not enough symbols available. {n_symbols} requested, but pool has {len(symbol_pool)}." )
182
183        symbol_categories = {}
184        alphabet = []
185        s_idx = 0
186
187        for i in range( n_categories ) :
188            for j in range( category_sizes[ i ] ) :
189                alphabet.append( symbol_pool[ s_idx ] )
190                symbol_categories[ symbol_pool[ s_idx ] ] = i
191                s_idx += 1
192
193        min_size = min( category_sizes )
194        max_size = max( category_sizes )
195
196        max_size_difference = max_size - min_size 
197        uniform = 1.0 / n_categories
198
199        category_cohesion = np.zeros( ( n_categories, n_categories ) )
200
201        if max_size_difference == 0 :
202            category_cohesion[ :, : ] = uniform
203
204        else :
205
206            # maximum small category bias
207            biased = min_category_size / np.array(category_sizes, dtype=float)
208
209            # apply bias factor
210            biased = small_category_bias * biased + ( 1.0 - small_category_bias ) * uniform
211
212            # normalized
213            biased = biased / np.sum( biased )
214
215            for i in range( n_categories ) :
216
217                # Small category is more uniformly selective, 
218                # Larger categories biased towareds selecting smaller categories
219                # r_i = ( category_sizes[i] - min_size ) / ( max_size - min_size )
220
221                r_i = 1.0 - 1.0 / math.pow( category_sizes[i], 0.5 )
222
223                # Intentionally not vectorized for readability
224                for j in range( n_categories ) :
225                    category_cohesion[ i, j ] = r_i * biased[ j ] + ( 1.0 - r_i ) * uniform
226
227                # repetition
228                category_cohesion[ i, i ] *= ( 1.0 - category_repetition_penalty )
229            
230                # normalize row
231                row_sum = np.sum( category_cohesion[ i, : ] )
232                if row_sum > 0 :
233                    category_cohesion[ i, : ] = category_cohesion[ i, : ] / row_sum
234        
235        symbol_cohesion = np.zeros( ( n_symbols, n_symbols ) )
236        for i, symb_a in enumerate( alphabet ) :
237            
238            c_a = symbol_categories[ symb_a ]
239            
240            for j, symb_b in enumerate( alphabet ) :
241                
242                c_b = symbol_categories[ symb_b ]
243                
244                symbol_cohesion[ i, j ] = category_cohesion[ c_a, c_b ] + category_cohesion[ c_a, c_b ] * rng.random() * within_category_variability
245
246                if i == j :
247                    symbol_cohesion[ i, j ] *= ( 1.0 - symbol_repetition_penalty )
248
249            row_sum = np.sum( symbol_cohesion[i, : ] )
250            if row_sum > 0 :
251                symbol_cohesion[i, : ] = symbol_cohesion[i, : ] / row_sum
252
253        return cls(
254            categories=set( symbol_categories.values() ),
255            category_cohesion=category_cohesion,
256            symbols=alphabet,
257            symbol_categories=symbol_categories,
258            symbol_cohesion=symbol_cohesion
259        )
StructuredSymbolSet( categories: set[int], category_cohesion: numpy.ndarray, symbols: list[str], symbol_categories: dict[str, int], symbol_cohesion: numpy.ndarray)
categories: set[int]
category_cohesion: numpy.ndarray
symbols: list[str]
symbol_categories: dict[str, int]
symbol_cohesion: numpy.ndarray
def save_json(self, filename: str, eoc_symbols: list[set[str]] | None = None) -> None:
20    def save_json( 
21        self, 
22        filename: str, 
23        eoc_symbols : list[set[str]] | None = None ) -> None :
24
25        if eoc_symbols is None :
26            eoc_symbols = []
27
28        data = {
29            "categories" : sorted(self.categories),
30            "category_cohesion" : self.category_cohesion.tolist(),
31            "symbols" : self.symbols,
32            "symbol_categories" : self.symbol_categories,
33            "symbol_cohesion" : self.symbol_cohesion.tolist(),
34            "eoc_symbols" : [ sorted( syms ) for syms in eoc_symbols ]
35        }
36        
37        with open(filename, 'w') as f:
38            json.dump(data, f, indent=4)
def symbol_counts_by_category(self) -> dict[int, int]:
40    def symbol_counts_by_category( self ) -> dict[int,int] : 
41        return dict(Counter(self.symbol_categories.values()))
def cyclic_orbit_size(self, invariant_categories: set[int] | None = None) -> int:
43    def cyclic_orbit_size(self, invariant_categories: set[int] | None = None) -> int: 
44        
45        if invariant_categories is None:
46            invariant_categories = set()
47
48        category_sizes = self.symbol_counts_by_category()
49
50        effective_sizes = [
51            size for category, size in category_sizes.items() 
52            if category not in invariant_categories
53        ]
54
55        if not effective_sizes:
56            return 1
57            
58        return math.lcm(*effective_sizes)
def category_symbols(self) -> dict[int, list[str]]:
60    def category_symbols( self ) -> dict[ int,list[str] ] : 
61        res = {}
62        for c in self.categories :
63            res[ c ] = sorted( [ 
64                s for s in self.symbols if self.symbol_categories[ s ] == c 
65            ] )
66        return res
@classmethod
def generate( cls, n_categories: int, symbols_per_category: int | list[int], rigidity: float, within_category_variability: float, category_repetition_penalty: float = 0.5, symbol_repetition_penalty: float = 0.85, np_rng: numpy.random._generator.Generator | None = None) -> StructuredSymbolSet:
 68    @classmethod
 69    def generate( 
 70        cls, 
 71        n_categories                : int,
 72        symbols_per_category        : int | list[int],
 73        rigidity                    : float,
 74        within_category_variability : float,
 75        category_repetition_penalty : float = 0.5,
 76        symbol_repetition_penalty   : float = 0.85,
 77        np_rng : np.random.Generator | None = None ) -> "StructuredSymbolSet" :
 78
 79        if isinstance( symbols_per_category, int ) :
 80            symbols_per_category = [symbols_per_category]*n_categories
 81
 82        elif len( symbols_per_category ) != n_categories :
 83            raise ValueError( "len( symbols_per_category ) != n_categories" )
 84
 85        if within_category_variability < 0.0 or within_category_variability > 1.0 :
 86            raise ValueError( f"within_category_variability should range from 0 to 1" )
 87
 88        if category_repetition_penalty < 0.0 or category_repetition_penalty > 1.0 :
 89            raise ValueError( f"category_repetition_penalty should range from 0 to 1" )
 90
 91        if symbol_repetition_penalty < 0.0 or symbol_repetition_penalty > 1.0 :
 92            raise ValueError( f"symbol_repetition_penalty should range from 0 to 1" )
 93
 94        rng = resolve_rng( np_rng )
 95
 96        n_symbols = sum( count for count in symbols_per_category )
 97
 98        symbol_pool = Vocabulary.digits() + Vocabulary.letters_lower() + Vocabulary.greek_lower() + Vocabulary.greek_upper()
 99
100        if len( symbol_pool ) < n_symbols :
101            raise ValueError( f"Too many symbols required, n_categories*symbols_per_category must be < {len(symbol_pool)}." )
102
103        symbol_categories = {}
104        alphabet = []
105        s_idx = 0
106
107        for i in range( n_categories ) :
108            for j in range( symbols_per_category[ i ] ) :
109                alphabet.append( symbol_pool[ s_idx ] )
110                symbol_categories[ symbol_pool[ s_idx ] ] = i
111                s_idx += 1
112
113        category_cohesion = np.zeros( ( n_categories, n_categories ) )
114        for i in range( n_categories ) :
115            
116            category_cohesion[ i, : ] = exp_uniform_blend( 
117                n=n_categories, 
118                alpha=(1.0-rigidity),
119                np_rng=rng )
120
121            category_cohesion[ i, i ] *= (1.0-category_repetition_penalty)
122
123        symbol_cohesion = np.zeros( ( n_symbols, n_symbols ) )
124        for i, symb_a in enumerate( alphabet ) :
125            for j, symb_b in enumerate( alphabet ) :
126                
127                c_a = symbol_categories[ symb_a ]
128                c_b = symbol_categories[ symb_b ]
129                
130                symbol_cohesion[ i, j ] = category_cohesion[ c_a, c_b ] + category_cohesion[ c_a, c_b ] * rng.random() * within_category_variability
131
132                if i == j :
133                    symbol_cohesion[ i, j ] *= ( 1.0 - symbol_repetition_penalty )
134
135        return cls(
136            categories=set( symbol_categories.values() ),
137            category_cohesion=category_cohesion,
138            symbols=alphabet,
139            symbol_categories=symbol_categories,
140            symbol_cohesion=symbol_cohesion
141        )
@classmethod
def generate_principled( cls, n_categories: int, min_category_size: int, category_growth_rate: float, small_category_bias: float, within_category_variability: float, category_repetition_penalty: float = 0.5, symbol_repetition_penalty: float = 0.85, np_rng: numpy.random._generator.Generator | None = None) -> StructuredSymbolSet:
143    @classmethod
144    def generate_principled( 
145        cls, 
146        n_categories                      : int,
147        min_category_size                 : int,
148        category_growth_rate              : float,
149        small_category_bias               : float,
150        within_category_variability       : float,
151        category_repetition_penalty       : float = 0.5,
152        symbol_repetition_penalty         : float = 0.85,
153        np_rng : np.random.Generator      | None = None ) -> "StructuredSymbolSet" :
154
155        if small_category_bias < 0.0 or small_category_bias > 1.0 :
156            raise ValueError( f"small_category_bias should range from 0 to 1" )
157
158        if within_category_variability < 0.0 or within_category_variability > 1.0 :
159            raise ValueError( f"within_category_variability should range from 0 to 1" )
160
161        if category_repetition_penalty < 0.0 or category_repetition_penalty > 1.0 :
162            raise ValueError( f"category_repetition_penalty should range from 0 to 1" )
163
164        if symbol_repetition_penalty < 0.0 or symbol_repetition_penalty > 1.0 :
165            raise ValueError( f"symbol_repetition_penalty should range from 0 to 1" )
166
167        rng = resolve_rng( np_rng )
168
169        category_sizes = [ min_category_size ]
170        last_term = min_category_size
171
172        for c in range( n_categories - 1 ) : 
173            next_term = last_term * category_growth_rate
174            category_sizes.append( int( round( next_term ) ) )
175            last_term = next_term
176
177        n_symbols   = sum( category_sizes )
178        symbol_pool = Vocabulary.digits() + Vocabulary.letters_lower() + Vocabulary.greek_lower() + Vocabulary.greek_upper()
179
180        if len( symbol_pool ) < n_symbols :
181            raise ValueError( f"Not enough symbols available. {n_symbols} requested, but pool has {len(symbol_pool)}." )
182
183        symbol_categories = {}
184        alphabet = []
185        s_idx = 0
186
187        for i in range( n_categories ) :
188            for j in range( category_sizes[ i ] ) :
189                alphabet.append( symbol_pool[ s_idx ] )
190                symbol_categories[ symbol_pool[ s_idx ] ] = i
191                s_idx += 1
192
193        min_size = min( category_sizes )
194        max_size = max( category_sizes )
195
196        max_size_difference = max_size - min_size 
197        uniform = 1.0 / n_categories
198
199        category_cohesion = np.zeros( ( n_categories, n_categories ) )
200
201        if max_size_difference == 0 :
202            category_cohesion[ :, : ] = uniform
203
204        else :
205
206            # maximum small category bias
207            biased = min_category_size / np.array(category_sizes, dtype=float)
208
209            # apply bias factor
210            biased = small_category_bias * biased + ( 1.0 - small_category_bias ) * uniform
211
212            # normalized
213            biased = biased / np.sum( biased )
214
215            for i in range( n_categories ) :
216
217                # Small category is more uniformly selective, 
218                # Larger categories biased towareds selecting smaller categories
219                # r_i = ( category_sizes[i] - min_size ) / ( max_size - min_size )
220
221                r_i = 1.0 - 1.0 / math.pow( category_sizes[i], 0.5 )
222
223                # Intentionally not vectorized for readability
224                for j in range( n_categories ) :
225                    category_cohesion[ i, j ] = r_i * biased[ j ] + ( 1.0 - r_i ) * uniform
226
227                # repetition
228                category_cohesion[ i, i ] *= ( 1.0 - category_repetition_penalty )
229            
230                # normalize row
231                row_sum = np.sum( category_cohesion[ i, : ] )
232                if row_sum > 0 :
233                    category_cohesion[ i, : ] = category_cohesion[ i, : ] / row_sum
234        
235        symbol_cohesion = np.zeros( ( n_symbols, n_symbols ) )
236        for i, symb_a in enumerate( alphabet ) :
237            
238            c_a = symbol_categories[ symb_a ]
239            
240            for j, symb_b in enumerate( alphabet ) :
241                
242                c_b = symbol_categories[ symb_b ]
243                
244                symbol_cohesion[ i, j ] = category_cohesion[ c_a, c_b ] + category_cohesion[ c_a, c_b ] * rng.random() * within_category_variability
245
246                if i == j :
247                    symbol_cohesion[ i, j ] *= ( 1.0 - symbol_repetition_penalty )
248
249            row_sum = np.sum( symbol_cohesion[i, : ] )
250            if row_sum > 0 :
251                symbol_cohesion[i, : ] = symbol_cohesion[i, : ] / row_sum
252
253        return cls(
254            categories=set( symbol_categories.values() ),
255            category_cohesion=category_cohesion,
256            symbols=alphabet,
257            symbol_categories=symbol_categories,
258            symbol_cohesion=symbol_cohesion
259        )