Package: comb-spec-searcher
Thecomb_spec_searcherpackage contains code for combinatorial
exploration.If this code is useful to you in your work, please consider citing it. To generate a
BibTeX entry (or another format), click the “DOI” badge above and locate the “Cite As”
section.InstallingTo installcomb_spec_searcheron your system, run:pipinstallcomb_spec_searcherIt is also possible to install comb_spec_searcher in development mode to
work on the source code, in which case you run the following after
cloning the repository:./setup.pydevelopCombinatorial explorationA (combinatorial) class is a set of objects with a notion of size such
that there are finitely many objects of each size. One of the primary
goals of enumerative combinatorics is to count how many objects of each
size there are in a class. One method for doing this is to find a
(combinatorial) specification, which is a collection of (combinatorial)
rules that describe how to build a class from other classes using
well-defined constructors. Such a specification can then be used to
count the number of objects of each size.Combinatorial explorationis a systematic application of strategies to
create rules about a class of interest, until a specification can be
found. This package can be used to perform this process automatically.
See theCombinatorial Exploration
projectandChristian Bean’s PhD
thesisfor more details.The remainder of this README will be an example of how to use this
package for performing combinatorial exploration on a specific class,
namely words avoiding consecutive patterns.Avoiding consecutive patterns in wordsA wordwover an alphabetΣis a string consisting of letters
fromΣ. We say thatwcontains the wordpif there is a
consecutive sequence of letters inwequal top. We saywavoidspif it does not containp. In this context, we callpa pattern. Inpython, this containment check can be checked
usingin.>>>fromcomb_spec_searcherimportCombinatorialObject>>>classWord(str,CombinatorialObject):...defsize(self):...returnstr.__len__(self)>>>w=Word("acbabcabbb")>>>p=Word("abcab")>>>pinwTrueFor an alphabetΣand a set of patternsPwe defineΣ*(P)to
be the set of words overΣthat avoid every pattern inP. These
are the classes that we will count. Of course, these all form regular
languages, but it will serve as a good example of how to use thecomb_spec_searcherpackage.The first step is to create the classes that will be used for
discovering the underlying structure of the class of interest. In this
case, considering the prefix of the words is what we need. We then
create a new pythonclassrepresenting this that inherits fromCombinatorialClasswhich can be imported fromcomb_spec_searcher.>>>fromcomb_spec_searcherimportCombinatorialClass>>>classAvoidingWithPrefix(CombinatorialClass[Word]):...def__init__(self,prefix,patterns,alphabet,just_prefix=False):...self.alphabet=frozenset(alphabet)...self.prefix=Word(prefix)...self.patterns=frozenset(map(Word,patterns))...self.just_prefix=just_prefix# this will be needed laterInheriting fromCombinatorialClassrequires you to implement a few
functions for combinatorial exploration:is_empty,to_jsonable,__eq__,__hash__,__repr__, and__str__.We will start by implementing the dunder methods (the ones with double
underscores) required. The__eq__method is particularly important
as theCombinatorialSpecificationSearcherwill use it to recognise
if the same class appears multiple times....# The dunder methods required to perform combinatorial exploration......def__eq__(self,other):...return(self.alphabet==other.alphabetand...self.prefix==other.prefixand...self.patterns==other.patternsand...self.just_prefix==other.just_prefix)......def__hash__(self):...returnhash(hash(self.prefix)+hash(self.patterns)+...hash(self.alphabet)+hash(self.just_prefix))......def__str__(self):...prefix=self.prefixifself.prefixelse'""'...ifself.just_prefix:...return"The word{}".format(prefix)...return("Words over {{{}}} avoiding {{{}}} with prefix{}"..."".format(", ".join(lforlinself.alphabet),...", ".join(pforpinself.patterns),...prefix))......def__repr__(self):...return"AvoidingWithPrefix({},{},{}".format(repr(self.prefix),...repr(self.patterns),...repr(self.alphabet))Perhaps the most important function to be implemented is theis_emptyfunction. This should returnTrueif there are no
objects of any size in the class, otherwiseFalse. If it is not
correctly implemented it may lead to tautological specifications. For
example, in our case the class is empty if and only if the prefix
contains a pattern to be avoided....defis_empty(self):...returnany(pinself.prefixforpinself.patterns)The final function required isto_jsonable. This is primarily for
the output, and only necessary for saving the output. It should be in a
format that can be interpretated byjson. What is important is that
thefrom_dictfunction is written in such a way that for any classcwe haveCombinatorialClass.from_dict(c.to_jsonable())== c....defto_jsonable(self):...return{"prefix":self.prefix,..."patterns":tuple(sorted(self.patterns)),..."alphabet":tuple(sorted(self.alphabet)),..."just_prefix":int(self.just_prefix)}......@classmethod...deffrom_dict(cls,data):...returncls(data['prefix'],...data['patterns'],...data['alphabet'],...bool(int(data['just_prefix'])))We also add some methods that we will need to get the enumerations of the
objects later....defis_atom(self):..."""Return True if the class contains a single word."""...returnself.just_prefix......defminimum_size_of_object(self):..."""Return the size of the smallest object in the class."""...returnlen(self.prefix)OurCombinatorialClassis now ready. What is left to do is create
the strategies that theCombinatorialSpecificationSearcherwill use
for performing combinatorial exploration. This is given in the form of aStrategyPackwhich can be imported fromcomb_spec_searcherthat
we will populate in the remainder of this example.>>>fromcomb_spec_searcherimportAtomStrategy,StrategyPack>>>pack=StrategyPack(initial_strats=[],...inferral_strats=[],...expansion_strats=[],...ver_strats=[AtomStrategy()],...name=("Finding specification for words avoiding "..."consecutive patterns."))Strategies are functions that take as input a classCand produce
rules aboutC. The types of strategies are as follows: -initial_strats: yields rules for classes -inferral_strats:
returns a single equivalence rule -expansion_strats: yields rules
for classes -ver_strats: returns a rule when the count of a class
is known.In our pack we have already added the AtomStrategy. This will verify any
combinatorial class that is an atom, in particular this is determined by theis_atommethod we implemented onCombinatorialClass. To get the
enumeration at the end, the strategy also uses the methodminimum_size_of_objectonCombinatorialClass. As we’ve implemented
these two methods already, we are free to use theAtomStrategy.Now we will create our first strategy. Every word over the alphabetΣstarting with prefixpis either justpor has prefixpafor someainΣ. This rule is splitting the original into disjoint subsets. We
call a rule using disjoint union aDisjointUnionStrategy. Although in this
case thereis a unique rule created by the strategy, strategies are assumed to
create multiple rules, and as such should be implemented as generators.>>>fromcomb_spec_searcherimportDisjointUnionStrategy>>>classExpansionStrategy(DisjointUnionStrategy[AvoidingWithPrefix,Word]):...defdecomposition_function(self,avoiding_with_prefix):...ifnotavoiding_with_prefix.just_prefix:...alphabet,prefix,patterns=(...avoiding_with_prefix.alphabet,...avoiding_with_prefix.prefix,...avoiding_with_prefix.patterns,...)...children=[AvoidingWithPrefix(prefix,patterns,alphabet,True)]...forainalphabet:...ends_with_a=AvoidingWithPrefix(prefix+a,patterns,alphabet)...children.append(ends_with_a)...returntuple(children)......defformal_step(self):...return"Either just the prefix, or append a letter from the alphabet"......defforward_map(self,avoiding_with_prefix,word,children=None):..."""
...        The backward direction of the underlying bijection used for object
...        generation and sampling.
...        """...assertisinstance(word,Word)...ifchildrenisNone:...children=self.decomposition_function(avoiding_with_prefix)...assertchildrenisnotNone...iflen(word)==len(avoiding_with_prefix.prefix):...return(word,)+tuple(Noneforiinrange(len(children)-1))...foridx,childinenumerate(children[1:]):...ifword[:len(child.prefix)]==child.prefix:...return(...tuple(Nonefor_inrange(idx+1))...+(word,)...+tuple(Nonefor_inrange(len(children)-idx-1))...)......def__str__(self):...returnself.formal_step()......def__repr__(self):...returnself.__class__.__name__+"()"......@classmethod...deffrom_dict(cls,d):...returncls()The final strategy we will need is one that peels off much as possible
from the front of the prefixpsuch that the avoidance conditions
are unaffected. This should then give a rule that is a cartesian product
of the part that is peeled off together with the words whose prefix is
that of the remainder of the original prefix. We call rules whose
constructor is cartesian product aDecompositionRule.>>>fromcomb_spec_searcherimportCartesianProductStrategy>>>classRemoveFrontOfPrefix(CartesianProductStrategy[AvoidingWithPrefix,Word]):...defdecomposition_function(self,avoiding_with_prefix):..."""If the k is the maximum size of a pattern to be avoided, then any
...        occurrence using indices further to the right of the prefix can use at
...        most the last k - 1 letters in the prefix."""...ifnotavoiding_with_prefix.just_prefix:...safe=self.index_safe_to_remove_up_to(avoiding_with_prefix)...ifsafe>0:...prefix,patterns,alphabet=(...avoiding_with_prefix.prefix,...avoiding_with_prefix.patterns,...avoiding_with_prefix.alphabet,...)...start_prefix=prefix[:safe]...end_prefix=prefix[safe:]...start=AvoidingWithPrefix(start_prefix,patterns,alphabet,True)...end=AvoidingWithPrefix(end_prefix,patterns,alphabet)...return(start,end)......defindex_safe_to_remove_up_to(self,avoiding_with_prefix):...prefix,patterns=(...avoiding_with_prefix.prefix,...avoiding_with_prefix.patterns,...)...# safe will be the index of the prefix in which we can remove upto without...# affecting the avoidance conditions...m=max(len(p)forpinpatterns)ifpatternselse1...safe=max(0,len(prefix)-m+1)...foriinrange(safe,len(prefix)):...end=prefix[i:]...ifany(end==patt[:len(end)]forpattinpatterns):...break...safe=i+1...returnsafe......defformal_step(self):...return"removing redundant prefix"......defbackward_map(self,avoiding_with_prefix,words,children=None):..."""
...        The forward direction of the underlying bijection used for object
...        generation and sampling.
...        """...assertlen(words)==2...assertisinstance(words[0],Word)...assertisinstance(words[1],Word)...ifchildrenisNone:...children=self.decomposition_function(avoiding_with_prefix)...assertchildrenisnotNone...yieldWord(words[0]+words[1])......defforward_map(self,comb_class,word,children=None):..."""
...        The backward direction of the underlying bijection used for object
...        generation and sampling.
...        """...assertisinstance(word,Word)...ifchildrenisNone:...children=self.decomposition_function(comb_class)...assertchildrenisnotNone...returnWord(children[0].prefix),Word(word[len(children[0].prefix):])......@classmethod...deffrom_dict(cls,d):...returncls()......def__str__(self):...returnself.formal_step()......def__repr__(self):...returnself.__class__.__name__+"()"With these three strategies we are now ready to perform combinatorial
exploration using the following pack.>>>pack=StrategyPack(initial_strats=[RemoveFrontOfPrefix()],...inferral_strats=[],...expansion_strats=[[ExpansionStrategy()]],...ver_strats=[AtomStrategy()],...name=("Finding specification for words avoiding "..."consecutive patterns."))First we need to create the combinatorial class we want to count. For
example, consider the words over the alphabet{a, b}that avoidababaandbabb. This class can be created using our initialise
function.>>>prefix=''>>>patterns=['ababa','babb']>>>alphabet=['a','b']>>>start_class=AvoidingWithPrefix(prefix,patterns,alphabet)We can then initialise ourCombinatorialSpecificationSearcher, and
use theauto_searchfunction which will return aCombinatorialSpecificationassuming one is found (which in this case always
will).>>>fromcomb_spec_searcherimportCombinatorialSpecificationSearcher>>>searcher=CombinatorialSpecificationSearcher(start_class,pack)>>>spec=searcher.auto_search()>>># spec.show() will display the specification in your browserNow that we have aCombinatorialSpecification, the obvious
thing we want to do is find the generating function for the class that
counts the number of objects of each size. This can be done by using theget_genfmethods onCombinatorialSpecification.Finally, in order to get initial terms, you will also need to implement
theobjects_of_sizefunction which should yield all of the objects
of a given size in the class.>>>fromitertoolsimportproduct>>>defobjects_of_size(self,size):..."""Yield the words of given size that start with prefix and avoid the
...     patterns. If just_prefix, then only yield that word."""...defpossible_words():..."""Yield all words of given size over the alphabet with prefix"""...iflen(self.prefix)>size:...return...forlettersinproduct(self.alphabet,...repeat=size-len(self.prefix)):...yieldWord(self.prefix+"".join(aforainletters))......ifself.just_prefix:...ifsize==len(self.prefix)andnotself.is_empty():...yieldWord(self.prefix)...return...forwordinpossible_words():...ifall(pattnotinwordforpattinself.patterns):...yieldword>>>AvoidingWithPrefix.objects_of_size=objects_of_sizeWith these in place if we then call theget_genffunction>>>spec.get_genf()-(x+1)*(x**2-x+1)**2*(x**2+x+1)/(x**6+x**3-x**2+2*x-1)we see that the the generating function isF =-(x**7+x**5+x**4+x**3+x**2+1)/(x**6+x**3-x**2+ 2*x - 1).Moreover, we can get directly the number of objects by size with the methodcount_objects_of_size.>>>[spec.count_objects_of_size(i)foriinrange(11)][1,2,4,8,15,27,48,87,157,283,511]You can now try this yourself using the fileexample.py, which can
count any set of words avoiding consecutive patterns.Now we will demonstrate how a bijection can be found between classes.
We will first need a couple of imports.>>>fromcomb_spec_searcher.bijectionimportParallelSpecFinder>>>fromcomb_spec_searcher.isomorphismimportBijectionWe start by defining our two classes that we wish to find a bijection between.>>>prefix1=''>>>patterns1=["00"]>>>alphabet1=['0','1']>>>class1=AvoidingWithPrefix(prefix1,patterns1,alphabet1)>>>prefix2=''>>>patterns2=["bb"]>>>alphabet2=['a','b']>>>class2=AvoidingWithPrefix(prefix2,patterns2,alphabet2)To find a bijection we expand the universe given a pack for both classes
and try to construct specifications that are parallel.>>>searcher1=CombinatorialSpecificationSearcher(class1,pack)>>>searcher2=CombinatorialSpecificationSearcher(class2,pack)We get two parallel specs if successful,Noneotherwise.>>>specs=ParallelSpecFinder(searcher1,searcher2).find()We then construct the bijection from the parallel specifications.>>>bijection=Bijection.construct(*specs)We can use theBijectionobject to map (either way) sampled objects
from the sepcifications.>>>foriinrange(10):...forwinbijection.domain.generate_objects_of_size(i):...assertw==bijection.inverse_map(bijection.map(w))...forwinbijection.codomain.generate_objects_of_size(i):...assertw==bijection.map(bijection.inverse_map(w))...Whether we find a bijection or not (when one exists) is highly
dependent on the packs chosen.CitingIf you found this library helpful with your research and would like to cite us,
you can use the followingBibTeXor go toZenodofor alternative formats.