comb-spec-searcher


Namecomb-spec-searcher JSON
Version 4.2.0 PyPI version JSON
download
home_pagehttps://github.com/PermutaTriangle/comb_spec_searcher
SummaryA library for performing combinatorial exploration.
upload_time2023-01-18 22:10:07
maintainer
docs_urlNone
authorPermuta Triangle
requires_python>=3.8
licenseBSD-3
keywords enumerative combinatorics combinatorial specification counting
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            Combinatorial Specification Searcher
====================================
.. image:: https://travis-ci.org/PermutaTriangle/comb_spec_searcher.svg?branch=master
    :alt: Travis
    :target: https://travis-ci.org/PermutaTriangle/comb_spec_searcher
.. image:: https://img.shields.io/coveralls/github/PermutaTriangle/comb_spec_searcher.svg
    :alt: Coveralls
    :target: https://coveralls.io/github/PermutaTriangle/comb_spec_searcher
.. image:: https://img.shields.io/pypi/v/comb_spec_searcher.svg
    :alt: PyPI
    :target: https://pypi.python.org/pypi/comb_spec_searcher
.. image:: https://img.shields.io/pypi/l/comb_spec_searcher.svg
    :target: https://pypi.python.org/pypi/comb_spec_searcher
.. image:: https://img.shields.io/pypi/pyversions/comb_spec_searcher.svg
    :target: https://pypi.python.org/pypi/comb_spec_searcher
.. image:: http://img.shields.io/badge/readme-tested-brightgreen.svg
    :alt: Travis
    :target: https://travis-ci.org/PermutaTriangle/comb_spec_searcher
.. image:: https://requires.io/github/PermutaTriangle/comb_spec_searcher/requirements.svg?branch=master
     :target: https://requires.io/github/PermutaTriangle/comb_spec_searcher/requirements/?branch=master
     :alt: Requirements Status
.. image:: https://zenodo.org/badge/121520109.svg
   :target: https://zenodo.org/badge/latestdoi/121520109

The ``comb_spec_searcher`` package 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.

Installing
----------

To install ``comb_spec_searcher`` on your system, run:

.. code:: bash

       pip install comb_spec_searcher

It 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:

.. code:: bash

       ./setup.py develop

Combinatorial exploration
-------------------------

A (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 exploration* is 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 the `Combinatorial Exploration
project <https://permutatriangle.github.io/papers/2019-02-27-combex.html>`__
and `Christian Bean’s PhD
thesis <https://opinvisindi.is/handle/20.500.11815/1184>`__ for 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 words
--------------------------------------

A word ``w`` over an alphabet ``Σ`` is a string consisting of letters
from ``Σ``. We say that ``w`` contains the word ``p`` if there is a
consecutive sequence of letters in ``w`` equal to ``p``. We say ``w``
avoids ``p`` if it does not contain ``p``. In this context, we call
``p`` a pattern. In ``python``, this containment check can be checked
using ``in``.

.. code:: python

   >>> from comb_spec_searcher import CombinatorialObject

   >>> class Word(str, CombinatorialObject):
   ...     def size(self):
   ...         return str.__len__(self)

   >>> w = Word("acbabcabbb")
   >>> p = Word("abcab")
   >>> p in w
   True

For an alphabet ``Σ`` and a set of patterns ``P`` we define ``Σ*(P)`` to
be the set of words over ``Σ`` that avoid every pattern in ``P``. 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 the
``comb_spec_searcher`` package.

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 python ``class`` representing this that inherits from
``CombinatorialClass`` which can be imported from
``comb_spec_searcher``.

.. code:: python

   >>> from comb_spec_searcher import CombinatorialClass


   >>> class AvoidingWithPrefix(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 later

Inheriting from ``CombinatorialClass`` requires 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 the ``CombinatorialSpecificationSearcher`` will use it to recognise
if the same class appears multiple times.

.. code:: python

   ...     # The dunder methods required to perform combinatorial exploration
   ...
   ...     def __eq__(self, other):
   ...         return (self.alphabet == other.alphabet and
   ...                 self.prefix == other.prefix and
   ...                 self.patterns == other.patterns and
   ...                 self.just_prefix == other.just_prefix)
   ...
   ...     def __hash__(self):
   ...         return hash(hash(self.prefix) + hash(self.patterns) +
   ...                     hash(self.alphabet) + hash(self.just_prefix))
   ...
   ...     def __str__(self):
   ...         prefix = self.prefix if self.prefix else '""'
   ...         if self.just_prefix:
   ...             return "The word {}".format(prefix)
   ...         return ("Words over {{{}}} avoiding {{{}}} with prefix {}"
   ...                 "".format(", ".join(l for l in self.alphabet),
   ...                           ", ".join(p for p in self.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 the
``is_empty`` function. This should return ``True`` if there are no
objects of any size in the class, otherwise ``False``. 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.

.. code:: python

   ...     def is_empty(self):
   ...         return any(p in self.prefix for p in self.patterns)

The final function required is ``to_jsonable``. This is primarily for
the output, and only necessary for saving the output. It should be in a
format that can be interpretated by ``json``. What is important is that
the ``from_dict`` function is written in such a way that for any class
``c`` we have ``CombinatorialClass.from_dict(c.to_jsonable()) == c``.

.. code:: python

   ...     def to_jsonable(self):
   ...         return {"prefix": self.prefix,
   ...                 "patterns": tuple(sorted(self.patterns)),
   ...                 "alphabet": tuple(sorted(self.alphabet)),
   ...                 "just_prefix": int(self.just_prefix)}
   ...
   ...     @classmethod
   ...     def from_dict(cls, data):
   ...         return cls(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.

.. code:: python

   ...     def is_atom(self):
   ...        """Return True if the class contains a single word."""
   ...        return self.just_prefix
   ...
   ...     def minimum_size_of_object(self):
   ...        """Return the size of the smallest object in the class."""
   ...        return len(self.prefix)

Our ``CombinatorialClass`` is now ready. What is left to do is create
the strategies that the ``CombinatorialSpecificationSearcher`` will use
for performing combinatorial exploration. This is given in the form of a
``StrategyPack`` which can be imported from ``comb_spec_searcher`` that
we will populate in the remainder of this example.

.. code:: python

   >>> from comb_spec_searcher import AtomStrategy, 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 class ``C`` and produce
rules about ``C``. 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 the
``is_atom`` method we implemented on ``CombinatorialClass``. To get the
enumeration at the end, the strategy also uses the method
``minimum_size_of_object`` on ``CombinatorialClass``. As we've implemented
these two methods already, we are free to use the ``AtomStrategy``.

Now we will create our first strategy. Every word over the alphabet ``Σ``
starting with prefix ``p`` is either just ``p`` or has prefix ``pa`` for some
``a`` in ``Σ``. This rule is splitting the original into disjoint subsets. We
call a rule using disjoint union a ``DisjointUnionStrategy``. 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.

.. code:: python

   >>> from comb_spec_searcher import DisjointUnionStrategy


   >>> class ExpansionStrategy(DisjointUnionStrategy[AvoidingWithPrefix, Word]):
   ...     def decomposition_function(self, avoiding_with_prefix):
   ...        if not avoiding_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)]
   ...           for a in alphabet:
   ...                 ends_with_a = AvoidingWithPrefix(prefix + a, patterns, alphabet)
   ...                 children.append(ends_with_a)
   ...           return tuple(children)
   ...
   ...     def formal_step(self):
   ...        return "Either just the prefix, or append a letter from the alphabet"
   ...
   ...     def forward_map(self, avoiding_with_prefix, word, children=None):
   ...        """
   ...        The backward direction of the underlying bijection used for object
   ...        generation and sampling.
   ...        """
   ...        assert isinstance(word, Word)
   ...        if children is None:
   ...           children = self.decomposition_function(avoiding_with_prefix)
   ...           assert children is not None
   ...        if len(word) == len(avoiding_with_prefix.prefix):
   ...           return (word,) + tuple(None for i in range(len(children) - 1))
   ...        for idx, child in enumerate(children[1:]):
   ...           if word[: len(child.prefix)] == child.prefix:
   ...                 return (
   ...                    tuple(None for _ in range(idx + 1))
   ...                    + (word,)
   ...                    + tuple(None for _ in range(len(children) - idx - 1))
   ...                 )
   ...
   ...     def __str__(self):
   ...        return self.formal_step()
   ...
   ...     def __repr__(self):
   ...        return self.__class__.__name__ + "()"
   ...
   ...     @classmethod
   ...     def from_dict(cls, d):
   ...        return cls()


The final strategy we will need is one that peels off much as possible
from the front of the prefix ``p`` such 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 a ``DecompositionRule``.

.. code:: python

   >>> from comb_spec_searcher import CartesianProductStrategy


   >>> class RemoveFrontOfPrefix(CartesianProductStrategy[AvoidingWithPrefix, Word]):
   ...     def decomposition_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."""
   ...        if not avoiding_with_prefix.just_prefix:
   ...           safe = self.index_safe_to_remove_up_to(avoiding_with_prefix)
   ...           if safe > 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)
   ...
   ...     def index_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) for p in patterns) if patterns else 1
   ...        safe = max(0, len(prefix) - m + 1)
   ...        for i in range(safe, len(prefix)):
   ...           end = prefix[i:]
   ...           if any(end == patt[: len(end)] for patt in patterns):
   ...                 break
   ...           safe = i + 1
   ...        return safe
   ...
   ...     def formal_step(self):
   ...        return "removing redundant prefix"
   ...
   ...     def backward_map(self, avoiding_with_prefix, words, children=None):
   ...        """
   ...        The forward direction of the underlying bijection used for object
   ...        generation and sampling.
   ...        """
   ...        assert len(words) == 2
   ...        assert isinstance(words[0], Word)
   ...        assert isinstance(words[1], Word)
   ...        if children is None:
   ...           children = self.decomposition_function(avoiding_with_prefix)
   ...           assert children is not None
   ...        yield Word(words[0] + words[1])
   ...
   ...     def forward_map(self, comb_class, word, children=None):
   ...        """
   ...        The backward direction of the underlying bijection used for object
   ...        generation and sampling.
   ...        """
   ...        assert isinstance(word, Word)
   ...        if children is None:
   ...           children = self.decomposition_function(comb_class)
   ...           assert children is not None
   ...        return Word(children[0].prefix), Word(word[len(children[0].prefix) :])
   ...
   ...     @classmethod
   ...     def from_dict(cls, d):
   ...        return cls()
   ...
   ...     def __str__(self):
   ...        return self.formal_step()
   ...
   ...     def __repr__(self):
   ...        return self.__class__.__name__ + "()"

With these three strategies we are now ready to perform combinatorial
exploration using the following pack.

.. code:: python

   >>> 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 avoid
``ababa`` and ``babb``. This class can be created using our initialise
function.

.. code:: python

   >>> prefix = ''
   >>> patterns = ['ababa', 'babb']
   >>> alphabet = ['a', 'b']
   >>> start_class = AvoidingWithPrefix(prefix, patterns, alphabet)

We can then initialise our ``CombinatorialSpecificationSearcher``, and
use the ``auto_search`` function which will return a
``CombinatorialSpecification`` assuming one is found (which in this case always
will).

.. code:: python

   >>> from comb_spec_searcher import CombinatorialSpecificationSearcher


   >>> searcher = CombinatorialSpecificationSearcher(start_class, pack)
   >>> spec = searcher.auto_search()
   >>> # spec.show() will display the specification in your browser

Now that we have a ``CombinatorialSpecification``, 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 the
``get_genf`` methods on ``CombinatorialSpecification``.

Finally, in order to get initial terms, you will also need to implement
the ``objects_of_size`` function which should yield all of the objects
of a given size in the class.

.. code:: python

   >>> from itertools import product

   >>> def objects_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."""
   ...     def possible_words():
   ...         """Yield all words of given size over the alphabet with prefix"""
   ...         if len(self.prefix) > size:
   ...            return
   ...         for letters in product(self.alphabet,
   ...                                 repeat=size - len(self.prefix)):
   ...             yield Word(self.prefix + "".join(a for a in letters))
   ...
   ...     if self.just_prefix:
   ...         if size == len(self.prefix) and not self.is_empty():
   ...             yield Word(self.prefix)
   ...         return
   ...     for word in possible_words():
   ...         if all(patt not in word for patt in self.patterns):
   ...             yield word
   >>> AvoidingWithPrefix.objects_of_size = objects_of_size

With these in place if we then call the ``get_genf`` function

.. code:: python

   >>> 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 is
``F = -(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 method
`count_objects_of_size`.

.. code:: python

   >>> [spec.count_objects_of_size(i) for i in range(11)]
   [1, 2, 4, 8, 15, 27, 48, 87, 157, 283, 511]

You can now try this yourself using the file ``example.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.

.. code:: python

   >>> from comb_spec_searcher.bijection import ParallelSpecFinder
   >>> from comb_spec_searcher.isomorphism import Bijection

We start by defining our two classes that we wish to find a bijection between.

.. code:: python

   >>> 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.

.. code:: python

   >>> searcher1 = CombinatorialSpecificationSearcher(class1, pack)
   >>> searcher2 = CombinatorialSpecificationSearcher(class2, pack)

We get two parallel specs if successful, ``None`` otherwise.

.. code:: python

   >>> specs = ParallelSpecFinder(searcher1, searcher2).find()

We then construct the bijection from the parallel specifications.

.. code:: python

   >>> bijection = Bijection.construct(*specs)

We can use the `Bijection` object to map (either way) sampled objects
from the sepcifications.

.. code:: python

   >>> for i in range(10):
   ...     for w in bijection.domain.generate_objects_of_size(i):
   ...         assert w == bijection.inverse_map(bijection.map(w))
   ...     for w in bijection.codomain.generate_objects_of_size(i):
   ...         assert w == bijection.map(bijection.inverse_map(w))
   ...

Whether we find a bijection or not (when one exists) is highly
dependent on the packs chosen.

Citing
######

If you found this library helpful with your research and would like to cite us,
you can use the following `BibTeX`_ or go to `Zenodo`_ for alternative formats.

.. _BibTex: https://zenodo.org/record/4944021/export/hx#.YMcpIC2l30o

.. _Zenodo: https://doi.org/10.5281/zenodo.4944020

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/PermutaTriangle/comb_spec_searcher",
    "name": "comb-spec-searcher",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "",
    "keywords": "enumerative combinatorics combinatorial specification counting",
    "author": "Permuta Triangle",
    "author_email": "permutatriangle@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/1e/6f/23490fd155fb125e57dd34a1281f0580171002d5707fab392caabb0a675e/comb_spec_searcher-4.2.0.tar.gz",
    "platform": null,
    "description": "Combinatorial Specification Searcher\n====================================\n.. image:: https://travis-ci.org/PermutaTriangle/comb_spec_searcher.svg?branch=master\n    :alt: Travis\n    :target: https://travis-ci.org/PermutaTriangle/comb_spec_searcher\n.. image:: https://img.shields.io/coveralls/github/PermutaTriangle/comb_spec_searcher.svg\n    :alt: Coveralls\n    :target: https://coveralls.io/github/PermutaTriangle/comb_spec_searcher\n.. image:: https://img.shields.io/pypi/v/comb_spec_searcher.svg\n    :alt: PyPI\n    :target: https://pypi.python.org/pypi/comb_spec_searcher\n.. image:: https://img.shields.io/pypi/l/comb_spec_searcher.svg\n    :target: https://pypi.python.org/pypi/comb_spec_searcher\n.. image:: https://img.shields.io/pypi/pyversions/comb_spec_searcher.svg\n    :target: https://pypi.python.org/pypi/comb_spec_searcher\n.. image:: http://img.shields.io/badge/readme-tested-brightgreen.svg\n    :alt: Travis\n    :target: https://travis-ci.org/PermutaTriangle/comb_spec_searcher\n.. image:: https://requires.io/github/PermutaTriangle/comb_spec_searcher/requirements.svg?branch=master\n     :target: https://requires.io/github/PermutaTriangle/comb_spec_searcher/requirements/?branch=master\n     :alt: Requirements Status\n.. image:: https://zenodo.org/badge/121520109.svg\n   :target: https://zenodo.org/badge/latestdoi/121520109\n\nThe ``comb_spec_searcher`` package contains code for combinatorial\nexploration.\n\nIf this code is useful to you in your work, please consider citing it. To generate a\nBibTeX entry (or another format), click the \"DOI\" badge above and locate the \"Cite As\"\nsection.\n\nInstalling\n----------\n\nTo install ``comb_spec_searcher`` on your system, run:\n\n.. code:: bash\n\n       pip install comb_spec_searcher\n\nIt is also possible to install comb_spec_searcher in development mode to\nwork on the source code, in which case you run the following after\ncloning the repository:\n\n.. code:: bash\n\n       ./setup.py develop\n\nCombinatorial exploration\n-------------------------\n\nA (combinatorial) class is a set of objects with a notion of size such\nthat there are finitely many objects of each size. One of the primary\ngoals of enumerative combinatorics is to count how many objects of each\nsize there are in a class. One method for doing this is to find a\n(combinatorial) specification, which is a collection of (combinatorial)\nrules that describe how to build a class from other classes using\nwell-defined constructors. Such a specification can then be used to\ncount the number of objects of each size.\n\n*Combinatorial exploration* is a systematic application of strategies to\ncreate rules about a class of interest, until a specification can be\nfound. This package can be used to perform this process automatically.\nSee the `Combinatorial Exploration\nproject <https://permutatriangle.github.io/papers/2019-02-27-combex.html>`__\nand `Christian Bean\u2019s PhD\nthesis <https://opinvisindi.is/handle/20.500.11815/1184>`__ for more details.\n\nThe remainder of this README will be an example of how to use this\npackage for performing combinatorial exploration on a specific class,\nnamely words avoiding consecutive patterns.\n\nAvoiding consecutive patterns in words\n--------------------------------------\n\nA word ``w`` over an alphabet ``\u03a3`` is a string consisting of letters\nfrom ``\u03a3``. We say that ``w`` contains the word ``p`` if there is a\nconsecutive sequence of letters in ``w`` equal to ``p``. We say ``w``\navoids ``p`` if it does not contain ``p``. In this context, we call\n``p`` a pattern. In ``python``, this containment check can be checked\nusing ``in``.\n\n.. code:: python\n\n   >>> from comb_spec_searcher import CombinatorialObject\n\n   >>> class Word(str, CombinatorialObject):\n   ...     def size(self):\n   ...         return str.__len__(self)\n\n   >>> w = Word(\"acbabcabbb\")\n   >>> p = Word(\"abcab\")\n   >>> p in w\n   True\n\nFor an alphabet ``\u03a3`` and a set of patterns ``P`` we define ``\u03a3*(P)`` to\nbe the set of words over ``\u03a3`` that avoid every pattern in ``P``. These\nare the classes that we will count. Of course, these all form regular\nlanguages, but it will serve as a good example of how to use the\n``comb_spec_searcher`` package.\n\nThe first step is to create the classes that will be used for\ndiscovering the underlying structure of the class of interest. In this\ncase, considering the prefix of the words is what we need. We then\ncreate a new python ``class`` representing this that inherits from\n``CombinatorialClass`` which can be imported from\n``comb_spec_searcher``.\n\n.. code:: python\n\n   >>> from comb_spec_searcher import CombinatorialClass\n\n\n   >>> class AvoidingWithPrefix(CombinatorialClass[Word]):\n   ...     def __init__(self, prefix, patterns, alphabet, just_prefix=False):\n   ...         self.alphabet = frozenset(alphabet)\n   ...         self.prefix = Word(prefix)\n   ...         self.patterns = frozenset(map(Word, patterns))\n   ...         self.just_prefix = just_prefix # this will be needed later\n\nInheriting from ``CombinatorialClass`` requires you to implement a few\nfunctions for combinatorial exploration: ``is_empty``, ``to_jsonable``,\n``__eq__``, ``__hash__``, ``__repr__``, and ``__str__``.\n\nWe will start by implementing the dunder methods (the ones with double\nunderscores) required. The ``__eq__`` method is particularly important\nas the ``CombinatorialSpecificationSearcher`` will use it to recognise\nif the same class appears multiple times.\n\n.. code:: python\n\n   ...     # The dunder methods required to perform combinatorial exploration\n   ...\n   ...     def __eq__(self, other):\n   ...         return (self.alphabet == other.alphabet and\n   ...                 self.prefix == other.prefix and\n   ...                 self.patterns == other.patterns and\n   ...                 self.just_prefix == other.just_prefix)\n   ...\n   ...     def __hash__(self):\n   ...         return hash(hash(self.prefix) + hash(self.patterns) +\n   ...                     hash(self.alphabet) + hash(self.just_prefix))\n   ...\n   ...     def __str__(self):\n   ...         prefix = self.prefix if self.prefix else '\"\"'\n   ...         if self.just_prefix:\n   ...             return \"The word {}\".format(prefix)\n   ...         return (\"Words over {{{}}} avoiding {{{}}} with prefix {}\"\n   ...                 \"\".format(\", \".join(l for l in self.alphabet),\n   ...                           \", \".join(p for p in self.patterns),\n   ...                           prefix))\n   ...\n   ...     def __repr__(self):\n   ...         return \"AvoidingWithPrefix({}, {}, {}\".format(repr(self.prefix),\n   ...                                                       repr(self.patterns),\n   ...                                                       repr(self.alphabet))\n\nPerhaps the most important function to be implemented is the\n``is_empty`` function. This should return ``True`` if there are no\nobjects of any size in the class, otherwise ``False``. If it is not\ncorrectly implemented it may lead to tautological specifications. For\nexample, in our case the class is empty if and only if the prefix\ncontains a pattern to be avoided.\n\n.. code:: python\n\n   ...     def is_empty(self):\n   ...         return any(p in self.prefix for p in self.patterns)\n\nThe final function required is ``to_jsonable``. This is primarily for\nthe output, and only necessary for saving the output. It should be in a\nformat that can be interpretated by ``json``. What is important is that\nthe ``from_dict`` function is written in such a way that for any class\n``c`` we have ``CombinatorialClass.from_dict(c.to_jsonable()) == c``.\n\n.. code:: python\n\n   ...     def to_jsonable(self):\n   ...         return {\"prefix\": self.prefix,\n   ...                 \"patterns\": tuple(sorted(self.patterns)),\n   ...                 \"alphabet\": tuple(sorted(self.alphabet)),\n   ...                 \"just_prefix\": int(self.just_prefix)}\n   ...\n   ...     @classmethod\n   ...     def from_dict(cls, data):\n   ...         return cls(data['prefix'],\n   ...                    data['patterns'],\n   ...                    data['alphabet'],\n   ...                    bool(int(data['just_prefix'])))\n\nWe also add some methods that we will need to get the enumerations of the\nobjects later.\n\n.. code:: python\n\n   ...     def is_atom(self):\n   ...        \"\"\"Return True if the class contains a single word.\"\"\"\n   ...        return self.just_prefix\n   ...\n   ...     def minimum_size_of_object(self):\n   ...        \"\"\"Return the size of the smallest object in the class.\"\"\"\n   ...        return len(self.prefix)\n\nOur ``CombinatorialClass`` is now ready. What is left to do is create\nthe strategies that the ``CombinatorialSpecificationSearcher`` will use\nfor performing combinatorial exploration. This is given in the form of a\n``StrategyPack`` which can be imported from ``comb_spec_searcher`` that\nwe will populate in the remainder of this example.\n\n.. code:: python\n\n   >>> from comb_spec_searcher import AtomStrategy, StrategyPack\n   >>> pack = StrategyPack(initial_strats=[],\n   ...                     inferral_strats=[],\n   ...                     expansion_strats=[],\n   ...                     ver_strats=[AtomStrategy()],\n   ...                     name=(\"Finding specification for words avoiding \"\n   ...                           \"consecutive patterns.\"))\n\nStrategies are functions that take as input a class ``C`` and produce\nrules about ``C``. The types of strategies are as follows: -\n``initial_strats``: yields rules for classes - ``inferral_strats``:\nreturns a single equivalence rule - ``expansion_strats``: yields rules\nfor classes - ``ver_strats``: returns a rule when the count of a class\nis known.\n\nIn our pack we have already added the AtomStrategy. This will verify any\ncombinatorial class that is an atom, in particular this is determined by the\n``is_atom`` method we implemented on ``CombinatorialClass``. To get the\nenumeration at the end, the strategy also uses the method\n``minimum_size_of_object`` on ``CombinatorialClass``. As we've implemented\nthese two methods already, we are free to use the ``AtomStrategy``.\n\nNow we will create our first strategy. Every word over the alphabet ``\u03a3``\nstarting with prefix ``p`` is either just ``p`` or has prefix ``pa`` for some\n``a`` in ``\u03a3``. This rule is splitting the original into disjoint subsets. We\ncall a rule using disjoint union a ``DisjointUnionStrategy``. Although in this\ncase thereis a unique rule created by the strategy, strategies are assumed to\ncreate multiple rules, and as such should be implemented as generators.\n\n.. code:: python\n\n   >>> from comb_spec_searcher import DisjointUnionStrategy\n\n\n   >>> class ExpansionStrategy(DisjointUnionStrategy[AvoidingWithPrefix, Word]):\n   ...     def decomposition_function(self, avoiding_with_prefix):\n   ...        if not avoiding_with_prefix.just_prefix:\n   ...           alphabet, prefix, patterns = (\n   ...                 avoiding_with_prefix.alphabet,\n   ...                 avoiding_with_prefix.prefix,\n   ...                 avoiding_with_prefix.patterns,\n   ...           )\n   ...           children = [AvoidingWithPrefix(prefix, patterns, alphabet, True)]\n   ...           for a in alphabet:\n   ...                 ends_with_a = AvoidingWithPrefix(prefix + a, patterns, alphabet)\n   ...                 children.append(ends_with_a)\n   ...           return tuple(children)\n   ...\n   ...     def formal_step(self):\n   ...        return \"Either just the prefix, or append a letter from the alphabet\"\n   ...\n   ...     def forward_map(self, avoiding_with_prefix, word, children=None):\n   ...        \"\"\"\n   ...        The backward direction of the underlying bijection used for object\n   ...        generation and sampling.\n   ...        \"\"\"\n   ...        assert isinstance(word, Word)\n   ...        if children is None:\n   ...           children = self.decomposition_function(avoiding_with_prefix)\n   ...           assert children is not None\n   ...        if len(word) == len(avoiding_with_prefix.prefix):\n   ...           return (word,) + tuple(None for i in range(len(children) - 1))\n   ...        for idx, child in enumerate(children[1:]):\n   ...           if word[: len(child.prefix)] == child.prefix:\n   ...                 return (\n   ...                    tuple(None for _ in range(idx + 1))\n   ...                    + (word,)\n   ...                    + tuple(None for _ in range(len(children) - idx - 1))\n   ...                 )\n   ...\n   ...     def __str__(self):\n   ...        return self.formal_step()\n   ...\n   ...     def __repr__(self):\n   ...        return self.__class__.__name__ + \"()\"\n   ...\n   ...     @classmethod\n   ...     def from_dict(cls, d):\n   ...        return cls()\n\n\nThe final strategy we will need is one that peels off much as possible\nfrom the front of the prefix ``p`` such that the avoidance conditions\nare unaffected. This should then give a rule that is a cartesian product\nof the part that is peeled off together with the words whose prefix is\nthat of the remainder of the original prefix. We call rules whose\nconstructor is cartesian product a ``DecompositionRule``.\n\n.. code:: python\n\n   >>> from comb_spec_searcher import CartesianProductStrategy\n\n\n   >>> class RemoveFrontOfPrefix(CartesianProductStrategy[AvoidingWithPrefix, Word]):\n   ...     def decomposition_function(self, avoiding_with_prefix):\n   ...        \"\"\"If the k is the maximum size of a pattern to be avoided, then any\n   ...        occurrence using indices further to the right of the prefix can use at\n   ...        most the last k - 1 letters in the prefix.\"\"\"\n   ...        if not avoiding_with_prefix.just_prefix:\n   ...           safe = self.index_safe_to_remove_up_to(avoiding_with_prefix)\n   ...           if safe > 0:\n   ...                 prefix, patterns, alphabet = (\n   ...                    avoiding_with_prefix.prefix,\n   ...                    avoiding_with_prefix.patterns,\n   ...                    avoiding_with_prefix.alphabet,\n   ...                 )\n   ...                 start_prefix = prefix[:safe]\n   ...                 end_prefix = prefix[safe:]\n   ...                 start = AvoidingWithPrefix(start_prefix, patterns, alphabet, True)\n   ...                 end = AvoidingWithPrefix(end_prefix, patterns, alphabet)\n   ...                 return (start, end)\n   ...\n   ...     def index_safe_to_remove_up_to(self, avoiding_with_prefix):\n   ...        prefix, patterns = (\n   ...           avoiding_with_prefix.prefix,\n   ...           avoiding_with_prefix.patterns,\n   ...        )\n   ...        # safe will be the index of the prefix in which we can remove upto without\n   ...        # affecting the avoidance conditions\n   ...        m = max(len(p) for p in patterns) if patterns else 1\n   ...        safe = max(0, len(prefix) - m + 1)\n   ...        for i in range(safe, len(prefix)):\n   ...           end = prefix[i:]\n   ...           if any(end == patt[: len(end)] for patt in patterns):\n   ...                 break\n   ...           safe = i + 1\n   ...        return safe\n   ...\n   ...     def formal_step(self):\n   ...        return \"removing redundant prefix\"\n   ...\n   ...     def backward_map(self, avoiding_with_prefix, words, children=None):\n   ...        \"\"\"\n   ...        The forward direction of the underlying bijection used for object\n   ...        generation and sampling.\n   ...        \"\"\"\n   ...        assert len(words) == 2\n   ...        assert isinstance(words[0], Word)\n   ...        assert isinstance(words[1], Word)\n   ...        if children is None:\n   ...           children = self.decomposition_function(avoiding_with_prefix)\n   ...           assert children is not None\n   ...        yield Word(words[0] + words[1])\n   ...\n   ...     def forward_map(self, comb_class, word, children=None):\n   ...        \"\"\"\n   ...        The backward direction of the underlying bijection used for object\n   ...        generation and sampling.\n   ...        \"\"\"\n   ...        assert isinstance(word, Word)\n   ...        if children is None:\n   ...           children = self.decomposition_function(comb_class)\n   ...           assert children is not None\n   ...        return Word(children[0].prefix), Word(word[len(children[0].prefix) :])\n   ...\n   ...     @classmethod\n   ...     def from_dict(cls, d):\n   ...        return cls()\n   ...\n   ...     def __str__(self):\n   ...        return self.formal_step()\n   ...\n   ...     def __repr__(self):\n   ...        return self.__class__.__name__ + \"()\"\n\nWith these three strategies we are now ready to perform combinatorial\nexploration using the following pack.\n\n.. code:: python\n\n   >>> pack = StrategyPack(initial_strats=[RemoveFrontOfPrefix()],\n   ...                     inferral_strats=[],\n   ...                     expansion_strats=[[ExpansionStrategy()]],\n   ...                     ver_strats=[AtomStrategy()],\n   ...                     name=(\"Finding specification for words avoiding \"\n   ...                           \"consecutive patterns.\"))\n\nFirst we need to create the combinatorial class we want to count. For\nexample, consider the words over the alphabet ``{a, b}`` that avoid\n``ababa`` and ``babb``. This class can be created using our initialise\nfunction.\n\n.. code:: python\n\n   >>> prefix = ''\n   >>> patterns = ['ababa', 'babb']\n   >>> alphabet = ['a', 'b']\n   >>> start_class = AvoidingWithPrefix(prefix, patterns, alphabet)\n\nWe can then initialise our ``CombinatorialSpecificationSearcher``, and\nuse the ``auto_search`` function which will return a\n``CombinatorialSpecification`` assuming one is found (which in this case always\nwill).\n\n.. code:: python\n\n   >>> from comb_spec_searcher import CombinatorialSpecificationSearcher\n\n\n   >>> searcher = CombinatorialSpecificationSearcher(start_class, pack)\n   >>> spec = searcher.auto_search()\n   >>> # spec.show() will display the specification in your browser\n\nNow that we have a ``CombinatorialSpecification``, the obvious\nthing we want to do is find the generating function for the class that\ncounts the number of objects of each size. This can be done by using the\n``get_genf`` methods on ``CombinatorialSpecification``.\n\nFinally, in order to get initial terms, you will also need to implement\nthe ``objects_of_size`` function which should yield all of the objects\nof a given size in the class.\n\n.. code:: python\n\n   >>> from itertools import product\n\n   >>> def objects_of_size(self, size):\n   ...     \"\"\"Yield the words of given size that start with prefix and avoid the\n   ...     patterns. If just_prefix, then only yield that word.\"\"\"\n   ...     def possible_words():\n   ...         \"\"\"Yield all words of given size over the alphabet with prefix\"\"\"\n   ...         if len(self.prefix) > size:\n   ...            return\n   ...         for letters in product(self.alphabet,\n   ...                                 repeat=size - len(self.prefix)):\n   ...             yield Word(self.prefix + \"\".join(a for a in letters))\n   ...\n   ...     if self.just_prefix:\n   ...         if size == len(self.prefix) and not self.is_empty():\n   ...             yield Word(self.prefix)\n   ...         return\n   ...     for word in possible_words():\n   ...         if all(patt not in word for patt in self.patterns):\n   ...             yield word\n   >>> AvoidingWithPrefix.objects_of_size = objects_of_size\n\nWith these in place if we then call the ``get_genf`` function\n\n.. code:: python\n\n   >>> spec.get_genf()\n   -(x + 1)*(x**2 - x + 1)**2*(x**2 + x + 1)/(x**6 + x**3 - x**2 + 2*x - 1)\n\nwe see that the the generating function is\n``F = -(x**7 + x**5 + x**4 + x**3 + x**2 + 1)/(x**6 + x**3 - x**2 + 2*x - 1)``.\n\nMoreover, we can get directly the number of objects by size with the method\n`count_objects_of_size`.\n\n.. code:: python\n\n   >>> [spec.count_objects_of_size(i) for i in range(11)]\n   [1, 2, 4, 8, 15, 27, 48, 87, 157, 283, 511]\n\nYou can now try this yourself using the file ``example.py``, which can\ncount any set of words avoiding consecutive patterns.\n\nNow we will demonstrate how a bijection can be found between classes.\nWe will first need a couple of imports.\n\n.. code:: python\n\n   >>> from comb_spec_searcher.bijection import ParallelSpecFinder\n   >>> from comb_spec_searcher.isomorphism import Bijection\n\nWe start by defining our two classes that we wish to find a bijection between.\n\n.. code:: python\n\n   >>> prefix1 = ''\n   >>> patterns1 = [\"00\"]\n   >>> alphabet1 = ['0', '1']\n   >>> class1 = AvoidingWithPrefix(prefix1, patterns1, alphabet1)\n   >>> prefix2 = ''\n   >>> patterns2 = [\"bb\"]\n   >>> alphabet2 = ['a', 'b']\n   >>> class2 = AvoidingWithPrefix(prefix2, patterns2, alphabet2)\n\nTo find a bijection we expand the universe given a pack for both classes\nand try to construct specifications that are parallel.\n\n.. code:: python\n\n   >>> searcher1 = CombinatorialSpecificationSearcher(class1, pack)\n   >>> searcher2 = CombinatorialSpecificationSearcher(class2, pack)\n\nWe get two parallel specs if successful, ``None`` otherwise.\n\n.. code:: python\n\n   >>> specs = ParallelSpecFinder(searcher1, searcher2).find()\n\nWe then construct the bijection from the parallel specifications.\n\n.. code:: python\n\n   >>> bijection = Bijection.construct(*specs)\n\nWe can use the `Bijection` object to map (either way) sampled objects\nfrom the sepcifications.\n\n.. code:: python\n\n   >>> for i in range(10):\n   ...     for w in bijection.domain.generate_objects_of_size(i):\n   ...         assert w == bijection.inverse_map(bijection.map(w))\n   ...     for w in bijection.codomain.generate_objects_of_size(i):\n   ...         assert w == bijection.map(bijection.inverse_map(w))\n   ...\n\nWhether we find a bijection or not (when one exists) is highly\ndependent on the packs chosen.\n\nCiting\n######\n\nIf you found this library helpful with your research and would like to cite us,\nyou can use the following `BibTeX`_ or go to `Zenodo`_ for alternative formats.\n\n.. _BibTex: https://zenodo.org/record/4944021/export/hx#.YMcpIC2l30o\n\n.. _Zenodo: https://doi.org/10.5281/zenodo.4944020\n",
    "bugtrack_url": null,
    "license": "BSD-3",
    "summary": "A library for performing combinatorial exploration.",
    "version": "4.2.0",
    "split_keywords": [
        "enumerative",
        "combinatorics",
        "combinatorial",
        "specification",
        "counting"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "1e6f23490fd155fb125e57dd34a1281f0580171002d5707fab392caabb0a675e",
                "md5": "d285c3f92b48f61145f2fc4971dc4e08",
                "sha256": "92df33f189dad10c7a5ee06a3ee305f383859e32718a09aa97a395154fecbfd7"
            },
            "downloads": -1,
            "filename": "comb_spec_searcher-4.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "d285c3f92b48f61145f2fc4971dc4e08",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 213884,
            "upload_time": "2023-01-18T22:10:07",
            "upload_time_iso_8601": "2023-01-18T22:10:07.214180Z",
            "url": "https://files.pythonhosted.org/packages/1e/6f/23490fd155fb125e57dd34a1281f0580171002d5707fab392caabb0a675e/comb_spec_searcher-4.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-01-18 22:10:07",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "PermutaTriangle",
    "github_project": "comb_spec_searcher",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "comb-spec-searcher"
}
        
Elapsed time: 8.29460s