python library with coLie computation objects
License: GPL3
unlisted
ubuntu2204##################################################################1#2# Data structures and methods for computing with coLie symbols3# © 2024 Benjamin Walter <[email protected]>4# (dedicated to the memory of Aydin Ozbek)5#6# Included classes:7# LieTree( bracket string )8# EilTree( symbol string )9# EilWord( assoc word )10# SignedWord( signed word )11#12# See docstrings for more information on use13#14##################################################################1516import re # used for weakly comparing trees171819class ValueTree():20"""ValueTree is class with attributes and methods common to trees of values2122Parameters23----------24value : str (default = "")25Value for the root vertex of tree (used to generate the rest of tree)26root : boolean (default = True)27Indicates if this is the root vertex (first call) of tree2829Attributes30----------31value : string32Value of this vertex33weight : integer34Weight (number of edges) supported by vertex35_branches : list of ValueTrees36Array of child nodes (subtrees)37"""383940_pos = 0 # use global position variable when scanning expressions to build trees414243def __init__(self,root=True):44self._branches = [] # list of branches out of vertex ("child trees")45self.value = "" # value tree stores a value at each vertex46self.weight = -1 # weight is total number of edges supported at vertex4748if root: # if this is not within a recursion49ValueTree._pos = 0 # then start reading at the beginning505152def __str__(self):53"""String version of tree is value at root"""54return self.value5556def __hash__(self):57"""Hash using the value of supported tree"""58return hash(self.value)5960def __len__(self):61"""weight is number of edges, len is number of vertices"""62return self.weight + 16364def letters(self):65"""Get letters and multiplicities from tree"""66return ''.join(sorted(re.sub(r'[/\W+/g]', '', self.value)))6768#########################69# & is 'weak pairing' -- verify that objects use same letters with same multiplicity70# this is used when computing pairings of symbols and Lie brackets71#72def __and__(self,other):73"""weak pairing of two objects: compare letters and multiplicity"""7475if self.weight != other.weight: # check lengths76return False7778for x in list(set(re.sub(r'[/\W+/g]', '', self.value))): # get list of unique letters79if not self.value.count(x) == other.value.count(x): # compare multiplicity of letters80return False81return True82#83##########################84858687def __iter__(self):88"""Dept-first iteration"""89yield self9091for branch in self._branches:92for twig in branch:93yield twig9495# note: maybe better to use chain and imap from itertools???96#97# from itertools import chain, imap98# for node in chain(*imap(iter, self._branches)):99# yield node100# yield self101102103#################104#105# currently unused methods, but interesting106#107def __bool__(self):108"""True if tree is nonempty"""109return self.weight != -1110111112def __eq__(self,other):113"""trees are equal if values at root are equal"""114return str(self) == str(other)115116117def __lt__(self,other):118"""ordering is by weight then value"""119return (self.weight, self.value) < (other.weight, other.value)120121122123##################################################################124##################################################################125126127class LieTree(ValueTree):128"""LieTree is a Lie bracket and tree of subbrackets, used to quickly get left and right subbrackets129130Parameters131----------132bracket : string (default "")133The Lie bracket to split apart. Use letters for generators, spaces are ignored, commas optional.134root : boolean (default True)135This parameter is only used interally when recursively building a tree. DON'T USE IT!136137Example138-------139bracket = LieTree("[ [a,b] , [ [c,d], e] ]")140141142Attributes143----------144value : string145The Lie bracket supported at this node146weight : integer147The weight (number of bracket symbols) of bracket148bracket : list of LieTrees149The left and right subbrackets150short : string151Short version of bracket (don't print ,)152"""153154155def __init__(self, bracket="", root=True):156super().__init__(root)157158if root: # on initial call, strip whitespace, comma, and ]159# (assume valid bracket input with single letter entries)160bracket = re.sub(r'[\],\s]', '', bracket)161162if bracket == "":163return164165# read left to right across the bracket expression using the _pos pointer to166# track where we are looking between recursive calls167168169if bracket[ValueTree._pos] == "[": # this node is a bracket of subexpressions170ValueTree._pos += 1 # advance past [171172self.bracket = [LieTree(bracket,False) , LieTree(bracket,False)]173174else: # this node is a single element175self.value = bracket[ValueTree._pos] # include element176self.weight = 0177ValueTree._pos += 1 # advance to next position178179180@property181def bracket(self):182return _branches183184185@bracket.setter186def bracket(self, bracket):187""""set left and right values of bracket -- only do this at a root!"""188self._branches = bracket189self.value = f'[{self._branches[0].value},{self._branches[1].value}]'190self.weight = 1 + self._branches[0].weight + self._branches[1].weight191192193@property194def left(self):195"""Left subbracket expression"""196197if self.weight > 0:198return self._branches[0]199200return None201202@property203def right(self):204"""Right subbracket expression"""205206if self.weight > 0:207return self._branches[1]208209return None210211@left.setter212def left(self, subbracket):213"""set left subbracket -- only do this at the root!"""214if len(self._branches) == 0:215self._branches.append(subbracket)216else:217self._branches[0] = subbracket218219if len(self._branches) == 2 and isinstance(self._branches[1],LieTree):220self.value = f'[{self._branches[0].value},{self._branches[1].value}]'221self.weight = 1 + self._branches[0].weight + self._branches[1].weight222223@right.setter224def right(self, subbracket):225"""set right subbracket -- only do this at the root!"""226if len(self._branches) == 0:227self._branches[0] = None228self._branches.append(subbracket)229elif len(self._branches) == 1:230self._branches.append(subbracket)231else:232self._branches[1] = subbracket233234if isinstance(self._branches[0],LieTree):235self.value = f'[{self._branches[0].value},{self._branches[1].value}]'236self.weight = 1 + self._branches[0].weight + self._branches[1].weight237238239@property240def short(self):241return re.sub(',','', self.value)242243@short.setter # you probably shouldn't change the bracket value of a node244def short(self,string):245value = list(string)246for i in reversed(range(1,len(value))):247if ((value[i-1].isalpha() and value[i].isalpha()) or248(value[i-1] == ']' and value[i].isalpha()) or249(value[i-1].isalpha() and value[i] == '[')):250value[i:i] = [',']251self.value = ''.join(value)252253254###################255#256# Interesting things that aren't currently used257#258def __mul__(self, other):259"""Overload multiplication to be Lie bracket"""260261if isinstance(other, LieTree):262product = LieTree()263264product.bracket = [ self , other ]265266return product267268return NotImplemented269270271def __repr__(self):272return f"LieTree('{self.value}')"273274275##################################################################276##################################################################277278class EilWord():279"""EilWord is a linear coLie symbol <=> associative word280EilWord objects have multiplication overloaded to perform pairing with Lie brackets and configuration braiding with group elements281282Parameters283----------284word : string (default "")285This is the linear symbol written as a word Example: abaa <=> (((a)b)a)a286287288Example289-------290word = EilWord("abaab")291292293Attributes294----------295value : string296The word297weight : integer298The weight of the corresponding symbol299symbol : string300The symbol corresponding to the associative eil word301"""302303304def __init__(self,word):305self.value = word306307308309def __mul__(self,other):310"""Multiplication is overloaded to compute pairing with Lie brackets and configuration braiding with group elements"""311312#########################313#314# Pairing with Lie Brackets recursively using bracket-cobracket compatibility from [SiWa]315#316if isinstance(other, LieTree):317318if not self.__weakPair(self.value,other): # check weakpairing319return 0320321return self.__pair(self.value,other) # recursively comput pairing322323324#########################325#326# Counting configuration braidings in words using algorithm from [GOSW]327# (This algorithm is originally due to Aydin Ozbek)328#329if isinstance(other, SignedWord): # Algorithm in [GOSW]:330331sum = [0] * len(self.value) # s value for each eil position332delta = [0] * len(self.value) # Δ value for each eil position333334for letter in other: # pass across the word335for i in range(len(self.value)): # evaluating eil symbol from leaf to root336337sum[i] += delta[i] # 1. add Δ value to s value338delta[i] = 0 # and set Δ to 0339340value = 0 # 2. get value of branches341342if self.value[i] == letter.value: # check if update is needed343if i == 0: # at first vertex there is no branch344value = 1345else: # otherwise look at value above346value = sum[i-1]347348if not letter: # 3. update s and Δ349sum[i] -= value # at inverses, immediately update s350else:351delta[i] = value # at generators, update Δ352353return sum[-1] + delta[-1] # Braiding value is s + Δ at root354355356return NotImplemented357358359360def __weakPair(self,word,lie):361"""weakPair is used internally to quickly rule out pairings that will be 0362It only compares multiplicity of letters appearing in word and Lie bracket363This is the analog of the & operation in ValueTree (which is used for general symbols)364"""365366if len(word) != len(lie): # compare length367return False368369for x in list(set(word)): # get list of unique letters370if not word.count(x) == lie.value.count(x): # compare multiplicity of letters371return False372return True373374375376def __pair(self,word,other):377"""pair is used internally to recursively compute pairings of eil words with Lie brackets378Pairing is computed by recursively using bracket-cobracket duality. Cobracket of eil words is given by cutting.379To speed things up, we compare size and letter grading (weakPair) before recursing. This immediately eliminates most 0 pairings.380"""381382if len(word) == 1: # Base case! Length 1 words are just a single generator383return int(word == other.value) # Check if generators match!384385pairing = 0 # Otherwise we use cobracket-bracket compatibility with pairing [SiWa]386387# < eil , lie > = < L(eil) , L(lie) > * < R(eil) , R(lie) >388# - < R(eil) , L(lie) > * < L(eil) , R(lie) >389#390# where ]eil[ = sum L(eil) x R(eil) and lie = [ L(lie) , R(lie) ]391392if self.__weakPair(word[:len(other.left)], other.left): # before recursing, weakpair start with left393pairing += self.__pair(word[:len(other.left)],other.left) * self.__pair(word[len(other.left):],other.right)394395if self.__weakPair(word[:len(other.right)], other.right): # before recursing, weakpair start with right396pairing -= self.__pair(word[:len(other.right)],other.right) * self.__pair(word[len(other.right):],other.left)397398399return pairing400401402#####################403#404# maybe we should allow eil(lie) instead of just eil * lie ???405#406def __call__(self, other):407return self * other408409410411def __str__(self):412return self.value413414def __hash__(self):415return hash(self.value);416417418###################419#420# Interesting things that aren't currently used421#422@property423def symbol(self):424n = len(self.value)-2425426symbol = ['('] * (n+1)427symbol.append(self.value[0])428for i in range(1,len(self.value)):429symbol.extend([')',self.value[i]])430431return ''.join(symbol)432433def letters(self):434return ''.join(sorted(self.value))435436@property437def weight(self):438return len(self.value)-1439440441def __len__(self):442return len(self.value)443444445def __repr__(self):446return f'EilWord("{self.value}")'447448449def __eq__(self,other):450return self.value == other.value451452453def __lt__(self,other):454return self.value < other.value455456457def cobracket(self):458"""This returns a generator which yields tuples of the cobracket (cutting the word at each position)"""459return ( (EilWord(self.value[:n]), EilWord(self.value[n:])) for n in range(1, len(self.value)) )460461462##################################################################463##################################################################464465class EilTree(ValueTree):466"""EilTree encodes the tree structure of a coLie symbol467EilTre objects have multiplication overloaded to perform pairing with Lie brackets and configuration braiding with group elements468469Parameters470----------471symbol : string (default "")472The symbol to split apart. Use letters for generators, spaces are ignored.473root : boolean (default True)474This parameter is only used interally when recursively building a tree. DON'T USE IT!475476Example477-------478symbol = EilTree("( ((a)(b)b) (b ((a)b)) a)")479480481Attributes482----------483value : string (default: "")484The symbol supported at this node485decoration : string (default: "")486The free variable of the symbol (at the given node)487weight : integer (default: -1)488The weight (number of parenthesis pairs) of symbol (supported by the given node)489subsymbols : list of EilTree's (default: [])490The immediate subsymbols of the given symbol491normalValue : string492A "normalized" version of the symbol -- move free variables to right, etc493"""494495def __init__(self, symbol="", root=True):496super().__init__(root)497498self.decoration = "" # keep track of decoration for each vertex499500if root: # on initial call, strip whitespace501symbol = symbol.translate(str.maketrans('', '', ' \n\t\r'))502503if symbol == "":504return505506# read left to right across the symbol expression using the _pos pointer to507# track where we are looking between recursive calls508509start = ValueTree._pos # this is the staring position for the subsymbol510511while ValueTree._pos < len(symbol) and not symbol[ValueTree._pos] == ")":512513if symbol[ValueTree._pos] == "(": # begin a subsymbol514ValueTree._pos += 1 # advance into subsymbol515self._branches.append(EilTree(symbol,False))# append new subtree516517else: # this is free variable (decoration of vertex)518self.decoration = symbol[ValueTree._pos] # record free variable519ValueTree._pos += 1 # advance past520521end = ValueTree._pos # this is the ending position for the subsymbol522523self.value = symbol[start:end] # this is the current subsymbol524525self.weight = sum([branch.weight + 1 for branch in self.subsymbols])526# self.weight = self.value.count('(') # this is equivalent but probably slower?527528ValueTree._pos += 1 # advance past the closing of subsymbol529530531532def __mul__(self, other):533"""Multiplication is overloaded to compute pairing with Lie brackets and configuration braiding with group elements"""534535#########################536#537# Pairing with Lie Brackets recursively using bracket-cobracket compatibility from [SiWa]538#539if isinstance(other, LieTree):540541if not self & other: # check weakpairing542return 0543544return self.__pair(other) # recursively compute pairing545546547#########################548#549# Counting configuration braidings in words using algorithm from [GOSW]550#551if isinstance(other, SignedWord):552counter = CountTree(self) # analog of sum and delta arrays for EilWord553554for letter in other.word:555counter.evaluate(letter) # evaluate the counter on each letter in the word556557return counter.total # this is s + Δ at root558559560return NotImplemented561562563564def __pair(self, other):565"""pair is used internally to recursively compute pairings of eil symbols with Lie brackets566Pairing is computed by recursively using bracket-cobracket duality.567Cobracket of symbols is given by cutting out a subtree, yielding sub and excised trees.568To speed things up, we compare size and letter grading (weakPair) before recursing. This immediately eliminates most 0 pairings.569"""570571if self.weight == 0: # Base case! Symbol is a single character572return int(self.value == other.value) # compare vs Lie value573574pairing = 0 # Otherwise we use cobracket-bracket compatibility with pairing [SiWa]575576# < eil , lie > = < L(eil) , L(lie) > * < R(eil) , R(lie) >577# - < R(eil) , L(lie) > * < L(eil) , R(lie) >578#579# where ]eil[ = sum L(eil) x R(eil) and lie = [ L(lie) , R(lie) ]580# L(eil) is subsymbol581# R(eil) is result of excising subsymbol582583for subsymbol in self: # iterate through all subsymbols of symbol584if subsymbol & other.left: # weakpair with subsymbol first, since excised symbol requires computation585pairing += subsymbol.__pair(other.left) * self.__excise(subsymbol).__pair(other.right)586587if subsymbol & other.right: # weakpair with subsymbol first, since excised symbol requires computation588pairing -= subsymbol.__pair(other.right) * self.__excise(subsymbol).__pair(other.left)589590return pairing591592# Note: this might be sped up by sorting subsymbols by weight initially rather than searching for weights matching other.left and other.right593594595596597def __excise(self, subsymbol):598"""excise is used internally to compute the symbol remaining after a subsymbol is removed599It recursively copies a tree, skipping the excised subsymbol (and its supported subtree).600"""601602#if self is subsymbol: # This should never happen during internal recursive usage!603# return EilTree()604605eil = EilTree() # Begin with a blank node606607eil.decoration = self.decoration # copy the decoration608609if self.weight == 0: # Direct copy if this is a leaf node610eil.weight = 0611eil.value = self.value612613else:614eil.subsymbols = [symbol.__excise(subsymbol) for symbol in self.subsymbols if not symbol is subsymbol]615616return eil617618@property619def subsymbols(self):620return self._branches621622@subsymbols.setter623def subsymbols(self, list):624self._branches = list625self.value = ''.join([f'({str(branch)})' for branch in self._branches]+[self.decoration])626self.weight = sum([branch.weight + 1 for branch in self._branches])627628# self.weight = self.value.count('(') # this should be equivalent, but probably slower?629630631# I like to write free elements last, so append() and extend() will actually insert at start...632def append(self, subsymbol):633"""Insert new subsymbol, updating weight and value"""634self._branches[:0] = [subsymbol] # insert subsymbol at start635self.weight += subsymbol.weight+1 # add to weight636self.value = f'({subsymbol.value}){self.value}' # add to value637638def extend(self, subsymbols):639"""Insert array of subsymbols, updating weight and value"""640self._branches[:0] = subsymbols641self.weight += sum([symbol.weight+1 for symbol in subsymbols])642tmp = ')('.join([symbol.value for symbol in subsymbols])643self.value = f'({tmp}){self.value}'644645def copy(self):646"""Create a deep copy of EilTree"""647eil = EilTree()648eil.decoration = self.decoration649eil.value , eil.weight = self.value , self.weight650651if len(self._branches) > 0:652eil._branches = [subsymbol.copy() for subsymbol in self._branches]653654return eil655656657###################658#659# Interesting things that aren't currently used660#661@property662def normalValue(self): # the normal Value has free terms written last663if self.weight == 0: # this should probably also sort branches nicely664return self.value # define < used for sorting later....665666return ''.join([f'({branch.normalValue})' for branch in sorted(self._branches,reverse=True)]+[self.decoration])667668669670def normalize(self):671"""Convert EilTree to 'normalized' form -- sort branches and move free letter to the far right"""672for symbol in self._branches:673symbol.normalize()674675#self.value = ''.join([f'({str(branch)})' for branch in sorted(self.branches,reverse=True)]+[self.decoration])676self.subsymbols = [branch for branch in sorted(self._branches,reverse=True)]677678679def __eq__(self,other): # compare normalized forms so (a)b == b(a)680if isinstance(other, EilTree): # Note: isn't perfect...681return self.normalValue == other.normalValue682683return False684685686# < is used when writing in normal form and comparing EilTrees687def __lt__(self,other):688if self.value == other.value: # if values match then these are equal689return False690691# next sort by weight and decoration692if (self.weight,self.decoration) < (other.weight,other.decoration):693return True694if (self.weight,self.decoration) > (other.weight,other.decoration):695return False696697# next sort by number of branches698if len(self.subsymbols) < len(other.subsymbols):699return True700if len(self.subsymbols) > len(other.subsymbols):701return False702703# next look at decorations and weights of branches704return sorted([(branch.decoration,branch.weight) for branch in self.subsymbols]) < sorted([(branch.decoration,branch.weight) for branch in other.subsymbols])705706# note: this may incorrectly sort a(a(b)) and a(a(c))707708709710def __gt__(self,other):711return other < self712713714def __repr__(self):715return f"EilTree('{self.value}')"716717718719################720#721# allow eil(lie) instead of just eil * lie722#723def __call__(self, other):724return self * other725726727def cobracket(self):728"""returns a generator with tuples of cobracket elements"""729subsymbols = iter(self)730next(subsymbols) # the first term in the iterator is the entire symbol (skip it)731732return ((subsymbol,self.__excise(subsymbol)) for subsymbol in subsymbols)733734##################################################################735##################################################################736737738class CountTree():739"""CountTree is a helper class for combinatorial braiding of coLie symbols on words.740This copies the structure of an EilTree and takes the place of the 'sum' and 'delta' arrays that were used in the __mul__ method of EilWord.741742Objects of this class should only be used internally!743744Parameters745----------746eil : EilTree747corresponding node of an EilTree748749Attributes750----------751sum : integer752current s value for this node (initialized to 0)753delta : integer754current Δ value for this node (initialized to 0)755branches : list of CountTree's756list of branches from this node757eil : EilTree758corresponding node of the EilTree759total : integer760sum + delta761"""762763764def __init__(self,eil):765self.sum = 0766self.delta = 0767self.eil = eil768769self.branches = [CountTree(branch) for branch in eil.subsymbols]770771772@property773def total(self):774return self.sum + self.delta775776777def evaluate(self,letter):778"""evaluate updates values of the counter tree at the given letter following the algorithm outlined in [GOSW]779Compare to the use of 'sum' and 'delta' arrays in the __mul__() method of EilWord.780"""781782783for branch in self.branches: # Evaluate leaf to root784branch.evaluate(letter) # (update branch values first)785786self.sum += self.delta # 1. add Δ to s787self.delta = 0 # and set Δ = 0788789value = 0 # 2. incorporate values from branches790791if letter.value == self.eil.decoration: # check if update is needed792if len(self.branches) == 0: # at leaf just copy the value793value = 1794else: # otherwise add values from branches795value = sum([branch.sum for branch in self.branches])796797798if not letter: # 3. at inverses, immediately update s799self.sum -= value800else: # otherwise, update Δ801self.delta = value802803804805###################806#807# Interesting things that aren't currently used808#809def __iter__(self):810for branch in self.branches:811for twig in branch:812yield twig813yield self814815816817##################################################################818##################################################################819820821class SignedLetter():822"""SignedLetter is a letter with a sign, intended to represent a generator or an inverse823* as a boolean it is True for generator and False for inverse824* as an integer it is 1 for generator and -1 for inverse825* as a string it looks pretty826827Parameters828----------829letter : string830this should be a single character831sign : boolean or integer832833Examples:834SignedLetter("a",False)835SignedLetter("a",-1)836837Attributes838----------839value : string840this is the letter841sign : boolean842True corresponds to generator843False correspnds to inverse844845"""846def __init__(self,letter,sign):847self.value = letter848self.sign = True if sign == 1 else False849850def __bool__(self):851return self.sign852853def __int__(self):854return 1 if self.sign else -1855856def __str__(self):857return f'{self.value}' if self.sign else f'{self.value}\N{SUPERSCRIPT MINUS}\N{SUPERSCRIPT ONE}'858859def __repr__(self):860return f'SignedLetter("{self.value}",{"1" if self.sign else "-1"})'861862def short(self):863return f'{self.value}{"" if self.sign else "-"}'864865def __hash__(self):866return hash(self.short())867868##################################################################869870871class SignedWord():872"""SignedWord is a list of SignedLetters873874Parameters875----------876word : string or list877878Examples:879SignedWord("aba^{-1}b^{-1}")880SignedWord("aba-b-")881SignedWord( [ ("a",1), ("b",1), ("a",-1), ("b",-1) ] )882883Attributes884----------885word : list of SignedLetters886"""887def __init__(self,word):888self.word = []889890if isinstance(word, str):891i = 0892while i < len(word):893if i+1 < len(word) and (word[i+1] == "-" or word[i+1] == "^"):894self.word.append(SignedLetter(word[i],-1))895else:896self.word.append(SignedLetter(word[i], 1))897i += 1898899while i < len(word) and not word[i].isalpha():900i += 1901elif isinstance(word, list):902if isinstance(word[0], SignedLetter):903self.word = word904else:905self.word = [SignedLetter(x[0],x[1]) for x in word]906else:907raise ValueError("Word format not recognized!")908909910def __len__(self):911return len(self.word)912913def __str__(self):914return ''.join([str(x) for x in self.word])915916def __repr__(self):917return 'SignedWord("' + ''.join([x.short() for x in self.word]) + '")'918919def __hash__(self):920return hash(''.join([x.short() for x in self.word]))921922def __iter__(self):923return iter(self.word)924925def letters(self):926return ''.join(sorted(set([letter.value for letter in self.word])))927928##################################################################929930