Positive Garside-factor tokenizer / garside_mvp.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import itertools, random, json
  2import numpy as np
  3
  4# Exact small-strand positive Garside arithmetic. Simple braids are indexed by
  5# permutations; all operations below are exact for n=4 (24 permutations).
  6def compose(p, q):
  7    return tuple(p[q[i]] for i in range(len(p)))
  8
  9def inverse(p):
 10    z = [0] * len(p)
 11    for i, v in enumerate(p): z[v] = i
 12    return tuple(z)
 13
 14def swap_perm(n, i):
 15    p = list(range(n)); p[i], p[i+1] = p[i+1], p[i]
 16    return tuple(p)
 17
 18def invcount(p):
 19    return sum(p[i] > p[j] for i in range(len(p)) for j in range(i+1, len(p)))
 20
 21def delta_word(n):
 22    return [i for row in range(n-1, 0, -1) for i in range(1, row+1)]
 23
 24def word_perm(word, n):
 25    p = tuple(range(n))
 26    for x in word: p = compose(p, swap_perm(n, x-1))
 27    return p
 28
 29def all_simples(n):
 30    return list(itertools.permutations(range(n)))
 31
 32def left_divides(a, b):
 33    # a divides b iff a^{-1}b is positive and lengths add; for simples this
 34    # is the left weak-order test.
 35    return invcount(a) + invcount(compose(inverse(a), b)) == invcount(b)
 36
 37def left_gcd(a, b, simples):
 38    common = [c for c in simples if left_divides(c, a) and left_divides(c, b)]
 39    return max(common, key=invcount)
 40
 41def normal_form_positive(word, n):
 42    """Canonical left-greedy factors for a positive generator word.
 43
 44    Append generators as singleton factors and repeatedly left-weight adjacent
 45    factors. For simples x,y, transfer gcd(partial(x), y) from y to x.
 46    """
 47    simples = all_simples(n); identity = tuple(range(n)); delta = tuple(reversed(range(n)))
 48    fs = []
 49    for g in word:
 50        fs.append(swap_perm(n, g-1))
 51        i = len(fs)-2
 52        while i >= 0:
 53            x, y = fs[i], fs[i+1]
 54            complement = compose(inverse(x), delta)
 55            a = left_gcd(complement, y, simples)
 56            if a == identity: break
 57            fs[i] = compose(x, a)
 58            fs[i+1] = compose(inverse(a), y)
 59            if fs[i+1] == identity: fs.pop(i+1)
 60            i -= 1
 61    return fs
 62
 63def factor_id(p):
 64    b = len(p)+1
 65    return sum(v*b**i for i,v in enumerate(p))
 66
 67def strip_left_deltas(word, n):
 68    """Remove literal removable Delta powers, then normalize the suffix.
 69
 70    This is exact for the augmentation used below (literal Delta^k inserted
 71    on the left). A full arbitrary-presentation Garside normalizer needs the
 72    usual complete cycling/transport implementation and is not claimed here.
 73    """
 74    d = delta_word(n); p = 0; pos = 0
 75    while word[pos:pos + len(d)] == d:
 76        p += 1; pos += len(d)
 77    return p, normal_form_positive(word[pos:], n)
 78
 79def check_identities(n=4):
 80    d = delta_word(n)
 81    relation = [word_perm(d+[i],n) == word_perm([n-i]+d,n) for i in range(1,n)]
 82    # Key local rewrite checks, including 121=Delta for B3.
 83    local = normal_form_positive([1,2,1],3)
 84    return {'delta_word_length':len(d),
 85            'delta_permutation_is_longest':word_perm(d,n)==tuple(reversed(range(n))),
 86            'delta_sigma_relation_all':all(relation),
 87            'delta_normal_form_factor_count':len(normal_form_positive(d,n)),
 88            'b3_121_normalizes_to_delta':local==[tuple(reversed(range(3)))]}
 89
 90def make_data(seed=7,n=4,ntrain=700,ntest=300):
 91    rng=random.Random(seed); d=delta_word(n); train=[]; test=[]
 92    for _ in range(ntrain):
 93        base=[rng.randint(1,n-1) for _ in range(rng.randint(8,22))]
 94        y=sum(x==1 for x in base)%2; train.append((base,y,0,base))
 95    for _ in range(ntest):
 96        base=[rng.randint(1,n-1) for _ in range(rng.randint(8,22))]
 97        y=sum(x==1 for x in base)%2; k=rng.randint(1,3)
 98        test.append((d*k+base,y,k,base))
 99    return train,test
100
101def featurize(examples,n,mode):
102    out=[]
103    for word,_,_,_ in examples:
104        if mode=='raw': z,size=word,n
105        else:
106            _,fs=strip_left_deltas(word,n); z=[factor_id(p) for p in fs]; size=(n+1)**n
107        v=np.bincount(z,minlength=size).astype(float); v/=max(1,len(z)); out.append(v)
108    return np.asarray(out)
109
110def nearest_centroid(x,y,tx):
111    cents=np.vstack([x[y==c].mean(0) for c in [0,1]])
112    return np.argmin(((tx[:,None,:]-cents[None,:,:])**2).sum(2),axis=1)
113
114def run(seed=7):
115    n=4; train,test=make_data(seed,n); metrics={}
116    for mode in ['raw','factor']:
117        x=featurize(train,n,mode); tx=featurize(test,n,mode)
118        y=np.array([e[1] for e in train]); ty=np.array([e[1] for e in test])
119        metrics[mode+'_accuracy']=float(np.mean(nearest_centroid(x,y,tx)==ty))
120    ratios=[]; exact=[]; raw_factor=[]
121    for word,_,k,base in test:
122        p,fs=strip_left_deltas(word,n); ratios.append(len(word)/max(1,len(fs)))
123        exact.append([factor_id(x) for x in fs]==[factor_id(x) for x in normal_form_positive(base,n)])
124        raw_factor.append((len(word),len(fs)))
125    metrics.update({'mean_presented_generator_length':float(np.mean([a for a,b in raw_factor])),
126      'mean_delta_free_factor_length':float(np.mean([b for a,b in raw_factor])),
127      'mean_generator_to_factor_ratio':float(np.mean(ratios)),
128      'delta_removal_factor_invariance':float(np.mean(exact)),
129      'expected_delta_added_generators':len(delta_word(n))})
130    return {'identity_check':check_identities(n),'metrics':metrics}
131
132if __name__=='__main__': print(json.dumps(run(),sort_keys=True,indent=2))