The tilings
Python library contains code for working with gridded permutations and tilings, and in particular the TileScope
algorithm, which can be used to enumerate permutation classes.
If you are primarily interested in enumerating permutation classes, then you may wish to skip ahead to the TileScope
section, but note the installation will be the same as for tilings
.
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.
If you need support, have a suggestion, or just want to be up to date with the latest developments please join us on our Discord server where we'd be happy to hear from you! To receive an email notification about major new releases, send an email to permutatriangle@gmail.com (but please direct all requests for assistance to the Discord server).
To install tilings
on your system, run:
pip install tilings
If you would like to edit the source code, you should install tilings
in development mode by cloning the repository, running
./setup.py develop
To verify that your installation is correct, you can try to get a specification for Av(12) by running in your terminal:
tilescope spec 12 point_placements
You should then be all set up to use tilings
and the TileScope
algorithm! The "Performance" section at the end of this document provides some more technical information.
We will be brief in our definitions here, for more details see Christian Bean’s PhD thesis.
A gridded permutation
is a pair (π, P)
where π
is a permutation and P
is a tuple of cells, called the positions, that denote the cells in which the points of π
are drawn on a grid. Let G
denote the set of all gridded permutations. Containment of gridded permutations is defined the same as containment of permutations, except including the preservation of the cells.
For example, (284376915, ((0, 0), (0, 3), (1, 1), (1, 1), (2, 3), (2, 2), (3, 4), (3, 0), (4, 2))
is drawn on the grid as follows.
+--+--+--+--+-+
| | | |● | |
+--+--+--+--+-+
| ●| | | | |
| | |● | | |
+--+--+--+--+-+
| | | ●| | |
| | | | |●|
+--+--+--+--+-+
| |● | | | |
| | ●| | | |
+--+--+--+--+-+
|● | | | | |
| | | | ●| |
+--+--+--+--+-+
A tiling
is a triple T = ((n, m), O, R)
, where n
and m
are positive integers, O
is a set of gridded permutations called obstructions
, and R
is a set of sets of gridded permutations called requirements
.
We say a gridded permutations avoids a set of gridded permutations if it avoids all of the permutations in the set, otherwise it contains the set. To contain a set, therefore, means to contain at least one in the set. The set of gridded permutations on a tiling Grid(T)
is the set of all gridded permutations in the n x m
grid that avoid O
and contain each set r
in R
.
Once you’ve installed tilings
, it can be imported by a Python script or an interactive Python session, just like any other Python library:
>>> from tilings import *
Importing *
from it supplies you with the GriddedPerm
and Tiling
classes.
As above, a gridded permutation is a pair (π, P)
where π
is a permutation and P
is a tuple of cells. The permutation is assumed to be a Perm
from the permuta
Python library. Not every tuple of cells is a valid position for a given permutation. This can be checked using the contradictory
method.
>>> from permuta import Perm
>>> gp = GriddedPerm(Perm((0, 2, 1)), ((0, 0), (0, 0), (1, 0)))
>>> gp.contradictory()
False
>>> gp = GriddedPerm(Perm((0, 1, 2)), ((0, 0), (0, 1), (0, 0)))
>>> gp.contradictory()
True
A Tiling
is created with an iterable of obstructions and an iterable of requirements (and each requirement is an iterable of gridded permutations). It is assumed that all cells not mentioned in some obstruction or requirement are empty. You can print the tiling to get an overview of the tiling created. In this example, we have a tiling that corresponds to non-empty permutations avoiding 123
.
>>> obstructions = [GriddedPerm.single_cell(Perm((0, 1)), (1, 1)),
... GriddedPerm.single_cell(Perm((1, 0)), (1, 1)),
... GriddedPerm.single_cell(Perm((0, 1)), (0, 0)),
... GriddedPerm.single_cell(Perm((0, 1, 2)), (2, 0)),
... GriddedPerm(Perm((0, 1, 2)), ((0, 0), (2, 0), (2, 0)))]
>>> requirements = [[GriddedPerm.single_cell(Perm((0,)), (1, 1))]]
>>> tiling = Tiling(obstructions, requirements)
>>> print(tiling)
+-+-+-+
| |●| |
+-+-+-+
|\| |1|
+-+-+-+
1: Av(012)
\: Av(01)
●: point
Crossing obstructions:
012: (0, 0), (2, 0), (2, 0)
Requirement 0:
0: (1, 1)
There are several properties of Tiling
that you can use, e.g.,
>>> tiling.dimensions
(3, 2)
>>> sorted(tiling.active_cells)
[(0, 0), (1, 1), (2, 0)]
>>> tiling.point_cells
frozenset({(1, 1)})
>>> sorted(tiling.possibly_empty)
[(0, 0), (2, 0)]
>>> tiling.positive_cells
frozenset({(1, 1)})
Those who have read ahead, or already started using tilings may have noticed that a Tiling
can also be defined with a third argument called assumptions
. These can be used to keep track of occurrences of gridded permutations on tilings. These are still in development but are essential for certain parts of the TileScope
algorithm. For simplicity we will not discuss these again until the Fusion section.
There are a number of methods available on the tiling. You can generate the gridded permutations satisfying the obstructions and requirements using the gridded_perms_of_length
method.
>>> for i in range(4):
... for gp in sorted(tiling.gridded_perms_of_length(i)):
... print(gp)
0: (1, 1)
01: (0, 0), (1, 1)
10: (1, 1), (2, 0)
021: (0, 0), (1, 1), (2, 0)
102: (0, 0), (0, 0), (1, 1)
120: (0, 0), (1, 1), (2, 0)
201: (1, 1), (2, 0), (2, 0)
210: (1, 1), (2, 0), (2, 0)
There are numerous other methods and properties. Many of these are specific to the TileScope
algorithm, discussed in Christian Bean’s PhD thesis. For the remainder of this readme we will focus on the TileScope
algorithm.
If you've not installed tilings
yet then go ahead and do this first by pip installing tilings
:
pip install tilings
Once done you can use the TileScope
algorithm in two ways, either directly by importing from the tilings.tilescope
module which we will discuss in greater detail shortly, or by using the TileScope
command line tool.
First, check the help commands for more information about its usage.
tilescope -h
tilescope spec -h
To search for a combinatorial specification use the subcommand tilescope spec
, e.g.
tilescope spec 231 point_placements
By default this command will try to solve for the generating function, although in some cases you will come across some not-yet-implemented features; for more information please join us on our Discord server, where we'd be happy to talk about it!
The point_placements
argument above is a strategy pack, which we explain in more detail in the StrategyPacks
section.
TileScope can be imported in a interactive Python session from tilings.tilescope
.
>>> from tilings.tilescope import *
Importing *
from tilings.tilescope
supplies you with the TileScope
and TileScopePack
classes. Running the TileScope
is as simple as choosing a class and a strategy pack. We'll go into more detail about the different strategies available shortly, but first let's enumerate our first permutation class. The example one always learns first in permutation patterns is enumerating Av(231). There are many different packs that will succeed for this class, but to get the most commonly described decomposition we can use point_placements
. The basis can be given to TileScope in several formats: an iterable of permuta.Perm, a string where the permutations are separated by '_'
(e.g. '231_4321'
), or as a Tiling
.
>>> pack = TileScopePack.point_placements()
>>> tilescope = TileScope('231', pack)
Once we have created our TileScope
we can then use the auto_search
method which will search for a specification using the strategies given. If successful it will return a CombinatorialSpecification. TileScope
uses logzero.logger
to report information. If you wish to suppress these prints, you can set logzero.loglevel
, which we have done here for sake of brevity in this readme!
>>> import logzero; import logging; logzero.loglevel(logging.CRITICAL)
>>> spec = tilescope.auto_search()
>>> print(spec)
A combinatorial specification with 4 rules.
-----------
0 -> (1, 2)
insert 0 in cell (0, 0)
+-+ +-+ +-+
|1| = | | + |1|
+-+ +-+ +-+
1: Av(120) 1: Av+(120)
Requirement 0:
0: (0, 0)
-------
1 -> ()
is atom
+-+
| |
+-+
<BLANKLINE>
-----
2 = 3
placing the topmost point in cell (0, 0), then row and column separation
+-+ +-+-+-+ +-+-+-+
|1| = | |●| | = | |●| |
+-+ +-+-+-+ +-+-+-+
1: Av+(120) |1| |1| | | |1|
Requirement 0: +-+-+-+ +-+-+-+
0: (0, 0) 1: Av(120) |1| | |
●: point +-+-+-+
Crossing obstructions: 1: Av(120)
10: (0, 0), (2, 0) ●: point
Requirement 0: Requirement 0:
0: (1, 1) 0: (1, 2)
---------
3 -> (0,)
tiling is locally factorable
+-+-+-+ +-+
| |●| | ↝ |1|
+-+-+-+ +-+
| | |1| 1: Av(120)
+-+-+-+
|1| | |
+-+-+-+
1: Av(120)
●: point
Requirement 0:
0: (1, 2)
The locally factorable tiling in the rule 3 -> (0,) could be further expanded down to atoms and the root tiling. This can be done using the expand_verified method.
>>> spec = spec.expand_verified()
>>> print(spec)
A combinatorial specification with 5 rules.
-----------
0 -> (1, 2)
insert 0 in cell (0, 0)
+-+ +-+ +-+
|1| = | | + |1|
+-+ +-+ +-+
1: Av(120) 1: Av+(120)
Requirement 0:
0: (0, 0)
-------
1 -> ()
is atom
+-+
| |
+-+
<BLANKLINE>
-----
2 = 3
placing the topmost point in cell (0, 0), then row and column separation
+-+ +-+-+-+ +-+-+-+
|1| = | |●| | = | |●| |
+-+ +-+-+-+ +-+-+-+
1: Av+(120) |1| |1| | | |1|
Requirement 0: +-+-+-+ +-+-+-+
0: (0, 0) 1: Av(120) |1| | |
●: point +-+-+-+
Crossing obstructions: 1: Av(120)
10: (0, 0), (2, 0) ●: point
Requirement 0: Requirement 0:
0: (1, 1) 0: (1, 2)
--------------
3 -> (0, 4, 0)
factor with partition {(0, 0)} / {(1, 2)} / {(2, 1)}
+-+-+-+ +-+ +-+ +-+
| |●| | = |1| x |●| x |1|
+-+-+-+ +-+ +-+ +-+
| | |1| 1: Av(120) ●: point 1: Av(120)
+-+-+-+ Requirement 0:
|1| | | 0: (0, 0)
+-+-+-+
1: Av(120)
●: point
Requirement 0:
0: (1, 2)
-------
4 -> ()
is atom
+-+
|●|
+-+
●: point
Requirement 0:
0: (0, 0)
Now that we have a specification we can do a number of things. For example, counting how many permutations there are in the class. This can be done using the count_objects_of_size
method on the CombinatorialSpecification.
>>> [spec.count_objects_of_size(i) for i in range(10)]
[1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862]
Of course we see the Catalan numbers! We can also sample uniformly using the random_sample_object_of_size
method. This will return a GriddedPerm
. We have used the ascii_plot
method for us to visualise it. If you want the underlying Perm
, this can be accessed with the patt
attribute. We also highlighted here the permuta.Perm.ascii_plot
method for an alternative visualisation.
>>> gp = spec.random_sample_object_of_size(10)
>>> print(gp) # doctest: +SKIP
9543102768: (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0)
>>> print(gp.ascii_plot()) # doctest: +SKIP
+----------+
|● |
| ●|
| ● |
| ● |
| ● |
| ● |
| ● |
| ● |
| ● |
| ● |
+----------+
>>> perm = gp.patt
>>> print(perm) # doctest: +SKIP
9543102768
>>> print(perm.ascii_plot()) # doctest: +SKIP
| | | | | | | | | |
-●-+-+-+-+-+-+-+-+-+-
| | | | | | | | | |
-+-+-+-+-+-+-+-+-+-●-
| | | | | | | | | |
-+-+-+-+-+-+-+-●-+-+-
| | | | | | | | | |
-+-+-+-+-+-+-+-+-●-+-
| | | | | | | | | |
-+-●-+-+-+-+-+-+-+-+-
| | | | | | | | | |
-+-+-●-+-+-+-+-+-+-+-
| | | | | | | | | |
-+-+-+-●-+-+-+-+-+-+-
| | | | | | | | | |
-+-+-+-+-+-+-●-+-+-+-
| | | | | | | | | |
-+-+-+-+-●-+-+-+-+-+-
| | | | | | | | | |
-+-+-+-+-+-●-+-+-+-+-
| | | | | | | | | |
You can use the get_equations
method which returns an iterator for the system of equations implied by the specification.
>>> list(spec.get_equations())
[Eq(F_0(x), F_1(x) + F_2(x)), Eq(F_1(x), 1), Eq(F_2(x), F_3(x)), Eq(F_3(x), F_0(x)**2*F_4(x)), Eq(F_4(x), x)]
You can also pass these directly to the solve
method in sympy
by using the get_genf
method. It will then return the solution which matches the initial conditions.
>>> spec.get_genf()
(1 - sqrt(1 - 4*x))/(2*x)
The sympy.solve
method can be very slow, particularly on big systems. If you are having troubles, then other softwares such as Mathematica and Maple are often better. You can also use the method get_maple_equations which will return a string containing Maple code for the equations.
>>> print(spec.get_maple_equations())
root_func := F[0, x]:
eqs := [
F[0, x] = (F[1, x] + F[2, x]),
F[1, x] = (1),
F[2, x] = F[3, x],
F[3, x] = ((F[0, x]**(2)) * F[4, x]),
F[4, x] = x
]:
count := [1, 1, 2, 5, 14, 42, 132]:
If you have a system of equations you are unable to solve, then please feel free to send them to our Discord server.
A specification can be saved and loaded later by converting it to JSON, a data storage format that can be written to a file or copy-pasted elsewhere for safe keeping. This functionality is built into TileScope. We can retrieve the JSON representation of a specification and load the specificiation from said JSON string by doing the following:
>>> import json
>>> from comb_spec_searcher import CombinatorialSpecification
>>> json_string = json.dumps(spec.to_jsonable())
>>> reloaded_spec = CombinatorialSpecification.from_dict(json.loads(json_string))
We have implemented a large number of structural decomposition strategies that we will discuss a bit more in the strategies section that follows. One can use any subset of these strategies to search for a combinatorial specification. This can be done by creating a TileScopePack
.
We have prepared a number of curated packs of strategies that we find to be rather effective. These can accessed as class methods on TileScopePack
. They are:
point_placements
: checks if cells are empty or not and places extreme points in cellsrow_and_col_placements
: places the left or rightmost points in columns, or the bottom or topmost points in rowsregular_insertion_encoding
: this pack includes the strategies required for finding the specification corresponding to a regular insertion encodinginsertion_row_and_col_placements
: this pack places rows and columns as above, but first ensures every active cell contains a point (this is in the same vein as the "slots" of the regular insertion encoding)insertion_point_placements
: places extreme points in cells, but first ensures every active cell contains a pointpattern_placements
: inserts size one requirements into a tiling, and then places points with respect to a pattern, e.g. if your permutation contains 123, then place the leftmost point that acts as a 2 in an occurrence of 123requirement_placements
: places points with respect to any requirement, e.g. if your permutation contains {12, 21}, then place the rightmost point that is either an occurrence of 1 in 12 or an occurrence of 2 in 21.only_root_placements
: this is the same aspattern_placements
except we only allow inserting into 1x1 tilings, therefore making it a finite packall_the_strategies
: a pack containing (almost) all of the strategies
Each of these packs have different parameters that can be set. You can view this by using the help command e.g., help(TileScopePack.pattern_placements)
. If you need help picking the right pack to enumerate your class join us on our Discord server where we'd be happy to help.
You can make any pack use the fusion strategy by using the method make_fusion
; for example, here is how to create the pack row_placements_fusion
.
>>> pack = TileScopePack.row_and_col_placements(row_only=True).make_fusion()
>>> print(pack)
Looking for recursive combinatorial specification with the strategies:
Inferral: row and column separation, obstruction transitivity
Initial: rearrange assumptions, add assumptions, factor, point corroboration, tracked fusion
Verification: verify atoms, insertion encoding verified, one by one verification, locally factorable verification
Set 1: row placement
This particular pack can be used to enumerate Av(123)
.
>>> tilescope = TileScope('123', pack)
>>> spec = tilescope.auto_search(smallest=True)
>>> print(spec) # doctest: +SKIP
A combinatorial specification with 10 rules.
-----------
0 -> (1, 2)
insert 0 in cell (0, 0)
+-+ +-+ +-+
|1| = | | + |1|
+-+ +-+ +-+
1: Av(012) 1: Av+(012)
Requirement 0:
0: (0, 0)
-------
1 -> ()
is atom
+-+
| |
+-+
-----------
3 -> (4, 5)
factor with partition {(0, 0), (0, 2)} / {(1, 1)}
+-+-+ +-+ +-+
|1| | = |1| x |●|
+-+-+ +-+ +-+
| |●| |\| ●: point
+-+-+ +-+ Requirement 0:
|\| | 1: Av(012) 0: (0, 0)
+-+-+ \: Av(01)
1: Av(012) Crossing obstructions:
\: Av(01) 012: (0, 0), (0, 1), (0, 1)
●: point
Crossing obstructions:
012: (0, 0), (0, 2), (0, 2)
Requirement 0:
0: (1, 1)
---------
3 -> (5,)
adding the assumption 'can count points in cell (0, 0)'
+-+-+ +-+-+
|\|1| ↣ |\|1|
+-+-+ +-+-+
1: Av(012) 1: Av(012)
\: Av(01) \: Av(01)
Crossing obstructions: Crossing obstructions:
012: (0, 0), (1, 0), (1, 0) 012: (0, 0), (1, 0), (1, 0)
Assumption 0:
can count points in cell (0, 0)
--------------
5 -> (1, 6, 7)
placing the topmost point in row 0
+-+-+ +-+ +-+-+-+ +-+-+-+-+
|\|1| = | | + |●| | | + | | |●| |
+-+-+ +-+ +-+-+-+ +-+-+-+-+
1: Av(012) | |\|1| |\|\| |1|
\: Av(01) +-+-+-+ +-+-+-+-+
Crossing obstructions: 1: Av(012) 1: Av(012)
012: (0, 0), (1, 0), (1, 0) \: Av(01) \: Av(01)
Assumption 0: ●: point ●: point
can count points in cell (0, 0) Crossing obstructions: Crossing obstructions:
012: (1, 0), (2, 0), (2, 0) 01: (0, 0), (1, 0)
Requirement 0: 012: (0, 0), (3, 0), (3, 0)
0: (0, 1) 012: (1, 0), (3, 0), (3, 0)
Assumption 0: Requirement 0:
can count points in cells (0, 1), (1, 0) 0: (2, 1)
Assumption 0:
can count points in cell (0, 0)
-----------
6 -> (8, 5)
factor with partition {(0, 1)} / {(1, 0), (2, 0)}
+-+-+-+ +-+ +-+-+
|●| | | = |●| x |\|1|
+-+-+-+ +-+ +-+-+
| |\|1| ●: point 1: Av(012)
+-+-+-+ Requirement 0: \: Av(01)
1: Av(012) 0: (0, 0) Crossing obstructions:
\: Av(01) Assumption 0: 012: (0, 0), (1, 0), (1, 0)
●: point can count points in cell (0, 0) Assumption 0:
Crossing obstructions: can count points in cell (0, 0)
012: (1, 0), (2, 0), (2, 0)
Requirement 0:
0: (0, 1)
Assumption 0:
can count points in cells (0, 1), (1, 0)
-------
8 -> ()
is atom
+-+
|●|
+-+
●: point
Requirement 0:
0: (0, 0)
Assumption 0:
can count points in cell (0, 0)
-----------
7 -> (9, 4)
factor with partition {(0, 0), (1, 0), (3, 0)} / {(2, 1)}
+-+-+-+-+ +-+-+-+ +-+
| | |●| | = |\|\|1| x |●|
+-+-+-+-+ +-+-+-+ +-+
|\|\| |1| 1: Av(012) ●: point
+-+-+-+-+ \: Av(01) Requirement 0:
1: Av(012) Crossing obstructions: 0: (0, 0)
\: Av(01) 01: (0, 0), (1, 0)
●: point 012: (0, 0), (2, 0), (2, 0)
Crossing obstructions: 012: (1, 0), (2, 0), (2, 0)
01: (0, 0), (1, 0) Assumption 0:
012: (0, 0), (3, 0), (3, 0) can count points in cell (0, 0)
012: (1, 0), (3, 0), (3, 0)
Requirement 0:
0: (2, 1)
Assumption 0:
can count points in cell (0, 0)
---------
9 -> (5,)
fuse columns 0 and 1
+-+-+-+ +-+-+
|\|\|1| ↣ |\|1|
+-+-+-+ +-+-+
1: Av(012) 1: Av(012)
\: Av(01) \: Av(01)
Crossing obstructions: Crossing obstructions:
01: (0, 0), (1, 0) 012: (0, 0), (1, 0), (1, 0)
012: (0, 0), (2, 0), (2, 0) Assumption 0:
012: (1, 0), (2, 0), (2, 0) can count points in cell (0, 0)
Assumption 0:
can count points in cell (0, 0)
-------
4 -> ()
is atom
+-+
|●|
+-+
●: point
Requirement 0:
0: (0, 0)
>>> [spec.count_objects_of_size(i) for i in range(10)]
[1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862]
It is possible to make your own pack as well, but for that you should first learn more about what the individual strategies do.
The TileScope
algorithm has in essence six different strategies that are applied in many different ways, resulting in very different universes in which to search for a combinatorial specification in. They are:
requirement insertions
: a disjoint union considering whether or not a tiling contains a requirementpoint placements
: places a uniquely defined point onto its own row and/or columnfactor
: when the obstructions and requirements become local to a set of cells, we factor out the local subtilingrow and column separation
: if all of the points in a cell in a row must appear below all of the other points in the row, then separate this onto its own row.obstruction inferral
: add obstructions that the requirements and obstructions of a tiling imply must be avoidedfusion
: merge two adjacent rows or columns of a tiling, if it can be viewed as a single row or column with a line drawn between
The simplest of all the arguments when enumerating permutation classes is to say, either a tiling is empty or contains a point. This can be viewed in tilings as either avoiding 1: (0, 0)
or containing 1: (0, 0)
.
>>> from tilings.strategies import CellInsertionFactory
>>> strategy_generator = CellInsertionFactory()
>>> tiling = Tiling.from_string('231')
>>> for strategy in strategy_generator(tiling):
... print(strategy(tiling))
insert 0 in cell (0, 0)
+-+ +-+ +-+
|1| = | | + |1|
+-+ +-+ +-+
1: Av(120) 1: Av+(120)
Requirement 0:
0: (0, 0)
The same underlying principle corresponds to avoiding or containing any set of gridded permutations. There are many different variations of this strategy used throughout our StrategyPacks
.
>>> import tilings
>>> print(tilings.strategies.requirement_insertion.__all__)
['CellInsertionFactory', 'RootInsertionFactory', 'RequirementExtensionFactory', 'RequirementInsertionFactory', 'FactorInsertionFactory', 'RequirementCorroborationFactory']
The core idea of this strategy is to place a uniquely defined point onto its own row and/or column. For example, here is a code snippet that shows the rules coming from placing the extreme (rightmost, topmost, leftmost, bottommost) points of a non-empty permutation avoiding 231
.
>>> from tilings.strategies import PatternPlacementFactory
>>> strategy = PatternPlacementFactory()
>>> tiling = Tiling.from_string('231').insert_cell((0,0))
>>> for rule in strategy(tiling):
... print(rule)
placing the rightmost point in cell (0, 0)
+-+ +-+-+
|1| = |\| |
+-+ +-+-+
1: Av+(120) | |●|
Requirement 0: +-+-+
0: (0, 0) |1| |
+-+-+
1: Av(120)
\: Av(01)
●: point
Crossing obstructions:
120: (0, 0), (0, 2), (0, 0)
Requirement 0:
0: (1, 1)
placing the topmost point in cell (0, 0)
+-+ +-+-+-+
|1| = | |●| |
+-+ +-+-+-+
1: Av+(120) |1| |1|
Requirement 0: +-+-+-+
0: (0, 0) 1: Av(120)
●: point
Crossing obstructions:
10: (0, 0), (2, 0)
Requirement 0:
0: (1, 1)
placing the leftmost point in cell (0, 0)
+-+ +-+-+
|1| = | |1|
+-+ +-+-+
1: Av+(120) |●| |
Requirement 0: +-+-+
0: (0, 0) | |1|
+-+-+
1: Av(120)
●: point
Crossing obstructions:
10: (1, 2), (1, 0)
Requirement 0:
0: (0, 1)
placing the bottommost point in cell (0, 0)
+-+ +-+-+-+
|1| = |\| |1|
+-+ +-+-+-+
1: Av+(120) | |●| |
Requirement 0: +-+-+-+
0: (0, 0) 1: Av(120)
\: Av(01)
●: point
Crossing obstructions:
120: (0, 1), (2, 1), (2, 1)
Requirement 0:
0: (1, 0)
Other algorithms used for automatically enumerating permutation classes have used variations of point placements. For example, enumeration schemes and the insertion encoding essentially consider placing the bottommost point into the row of a tiling. Here is a code snippet for calling a strategy that places points into a row of a tiling.
>>> from permuta.misc import DIR_SOUTH
>>> from tilings.strategies import RowAndColumnPlacementFactory
>>> strategy = RowAndColumnPlacementFactory(place_row=True, place_col=False)
>>> placed_tiling = tiling.place_point_in_cell((0, 0), DIR_SOUTH)
>>> for rule in strategy(placed_tiling):
... print(rule)
placing the topmost point in row 1
+-+-+-+ +-+ +-+-+-+-+ +-+-+-+-+-+
|\| |1| = |●| + |●| | | | + | | | |●| |
+-+-+-+ +-+ +-+-+-+-+ +-+-+-+-+-+
| |●| | ●: point | |\| |1| |\| |1| |1|
+-+-+-+ Requirement 0: +-+-+-+-+ +-+-+-+-+-+
1: Av(120) 0: (0, 0) | | |●| | | |●| | | |
\: Av(01) +-+-+-+-+ +-+-+-+-+-+
●: point 1: Av(120) 1: Av(120)
Crossing obstructions: \: Av(01) \: Av(01)
120: (0, 1), (2, 1), (2, 1) ●: point ●: point
Requirement 0: Crossing obstructions: Crossing obstructions:
0: (1, 0) 120: (1, 1), (3, 1), (3, 1) 10: (0, 1), (4, 1)
Requirement 0: 10: (2, 1), (4, 1)
0: (0, 2) 120: (0, 1), (2, 1), (2, 1)
Requirement 1: Requirement 0:
0: (2, 0) 0: (1, 0)
Requirement 1:
0: (3, 2)
placing the bottommost point in row 1
+-+-+-+ +-+ +-+-+-+-+ +-+-+-+-+-+
|\| |1| = |●| + |\| | |1| + |\| |\| |1|
+-+-+-+ +-+ +-+-+-+-+ +-+-+-+-+-+
| |●| | ●: point | |●| | | | | | |●| |
+-+-+-+ Requirement 0: +-+-+-+-+ +-+-+-+-+-+
1: Av(120) 0: (0, 0) | | |●| | | |●| | | |
\: Av(01) +-+-+-+-+ +-+-+-+-+-+
●: point 1: Av(120) 1: Av(120)
Crossing obstructions: \: Av(01) \: Av(01)
120: (0, 1), (2, 1), (2, 1) ●: point ●: point
Requirement 0: Crossing obstructions: Crossing obstructions:
0: (1, 0) 120: (0, 2), (3, 2), (3, 2) 01: (0, 2), (2, 2)
Requirement 0: 120: (0, 2), (4, 2), (4, 2)
0: (1, 1) 120: (2, 2), (4, 2), (4, 2)
Requirement 1: Requirement 0:
0: (2, 0) 0: (1, 0)
Requirement 1:
0: (3, 1)
Every non-empty permutation in Av(231)
can be written in the form αnβ where α
, β
are permutation avoiding 231
, and all of the values in α
are below all of the values in β
. The tiling representing placing the topmost point in Av(231)
contains a crossing size 2 obstruction 10: (0, 0), (2, 0)
. This obstruction precisely says that the points in the cell (0, 0)
must appear below the points in the cell (2, 0)
. The RowColumnSeparationStrategy
will try to separate the rows and columns as much as possible according to the size two crossing obstructions.
>>> from permuta.misc import DIR_NORTH
>>> from tilings.strategies import RowColumnSeparationStrategy
>>> strategy = RowColumnSeparationStrategy()
>>> placed_tiling = tiling.place_point_in_cell((0, 0), DIR_NORTH)
>>> rule = strategy(placed_tiling)
>>> print(rule)
row and column separation
+-+-+-+ +-+-+-+
| |●| | = | |●| |
+-+-+-+ +-+-+-+
|1| |1| | | |1|
+-+-+-+ +-+-+-+
1: Av(120) |1| | |
●: point +-+-+-+
Crossing obstructions: 1: Av(120)
10: (0, 0), (2, 0) ●: point
Requirement 0: Requirement 0:
0: (1, 1) 0: (1, 2)
If there are no crossing obstructions between two cells a
and b
on a tiling then the choice of points in a
are independent from the choice of points in b
.
>>> separated_tiling = rule.children[0]
>>> from tilings.strategies import FactorFactory
>>> strategy_generator = FactorFactory()
>>> for strategy in strategy_generator(separated_tiling):
... print(strategy(separated_tiling))
factor with partition {(0, 0)} / {(1, 2)} / {(2, 1)}
+-+-+-+ +-+ +-+ +-+
| |●| | = |1| x |●| x |1|
+-+-+-+ +-+ +-+ +-+
| | |1| 1: Av(120) ●: point 1: Av(120)
+-+-+-+ Requirement 0:
|1| | | 0: (0, 0)
+-+-+-+
1: Av(120)
●: point
Requirement 0:
0: (1, 2)
The x
in the printed above rule is used to denote Cartesian product. We do this to signify that there is a size-preserving bijection between the gridded permutations on the left-hand side, to the set of 3-tuples coming from the Cartesian product on the right-hand side, where the size of a tuple is the sum of the sizes of the parts. In particular, it implies that the enumeration of the gridded permutations on the left-hand side can be computed by applying the Cauchy product to the enumerations of the three sets of gridded permutations on the right-hand side.
To guarantee that these rules are always counted using the Cauchy product we must also ensure any two cells on the same row or column are also contained in the same factor, otherwise when counting the left-hand side we have to consider the possible interleavings going on.
>>> tiling = Tiling.from_string('231_132').insert_cell((0,0))
>>> placed_tiling = tiling.place_point_in_cell((0, 0), DIR_SOUTH)
>>> strategy_generator = FactorFactory()
>>> for strategy in strategy_generator(placed_tiling):
... print(strategy(placed_tiling))
factor with partition {(0, 1), (2, 1)} / {(1, 0)}
+-+-+-+ +-+-+ +-+
|\| |/| = |\|/| x |●|
+-+-+-+ +-+-+ +-+
| |●| | /: Av(10) ●: point
+-+-+-+ \: Av(01) Requirement 0:
/: Av(10) 0: (0, 0)
\: Av(01)
●: point
Requirement 0:
0: (1, 0)
Using the setting all
in FactorFactory
will allow us to factor according to only the obstructions and requirements.
>>> strategy_generator = FactorFactory('all')
>>> for strategy in strategy_generator(placed_tiling):
... print(strategy(placed_tiling))
interleaving factor with partition {(0, 1)} / {(1, 0)} / {(2, 1)}
+-+-+-+ +-+ +-+ +-+
|\| |/| = |\| * |●| * |/|
+-+-+-+ +-+ +-+ +-+
| |●| | \: Av(01) ●: point /: Av(10)
+-+-+-+ Requirement 0:
/: Av(10) 0: (0, 0)
\: Av(01)
●: point
Requirement 0:
0: (1, 0)
We instead use the symbol *
to make us aware that this is not counted by the Cauchy product, but we must also count the possible interleavings.
The presence of requirements alongside the obstructions on a tiling can sometimes be used to imply that all of the gridded permutations on a tiling also avoid some additional obstruction. The goal of ObstructionInferral
is to add these to a tiling.
>>> from permuta.misc import DIR_NORTH
>>> tiling = Tiling.from_string('1234_1243_1423_4123')
>>> placed_tiling = tiling.partial_place_point_in_cell((0, 0), DIR_NORTH)
>>> from tilings.strategies import ObstructionInferralFactory
>>> strategy_generator = ObstructionInferralFactory(3)
>>> for strategy in strategy_generator(placed_tiling):
... print(strategy(placed_tiling))
added the obstructions {012: (0, 0), (0, 0), (0, 0)}
+-+ +-+
|●| = |●|
+-+ +-+
|1| |1|
+-+ +-+
1: Av(0123, 0132, 0312, 3012) 1: Av(012)
●: point ●: point
Crossing obstructions: Requirement 0:
0123: (0, 0), (0, 0), (0, 0), (0, 1) 0: (0, 1)
0132: (0, 0), (0, 0), (0, 1), (0, 0)
0312: (0, 0), (0, 1), (0, 0), (0, 0)
3012: (0, 1), (0, 0), (0, 0), (0, 0)
Requirement 0:
0: (0, 1)
In the above code snippet, we have added the obstruction gp = 012: (0, 0), (0, 0), (0, 0)
. In particular, the 4 crossing obstructions, and the 4 localised obstructions, all contained a copy of gp
, so we simplify the right-hand side by removing these from the tiling. This simplification step happens automatically when creating a Tiling
.
Consider the gridded permutations on the following tiling.
>>> tiling = Tiling([GriddedPerm(Perm((0, 1)), ((0, 0), (0, 0))), GriddedPerm(Perm((0, 1)), ((0, 0), (1, 0))), GriddedPerm(Perm((0, 1)), ((1, 0), (1, 0)))])
>>> print(tiling)
+-+-+
|\|\|
+-+-+
\: Av(01)
Crossing obstructions:
01: (0, 0), (1, 0)
>>> for i in range(4):
... for gp in sorted(tiling.gridded_perms_of_length(i)):
... print(gp)
ε:
0: (0, 0)
0: (1, 0)
10: (0, 0), (0, 0)
10: (0, 0), (1, 0)
10: (1, 0), (1, 0)
210: (0, 0), (0, 0), (0, 0)
210: (0, 0), (0, 0), (1, 0)
210: (0, 0), (1, 0), (1, 0)
210: (1, 0), (1, 0), (1, 0)
Due to the crossing 01
obstruction it is clear that all of the underlying permutations will be decreasing. Moreover, the transition between the left cell and the right cell can be between any of the points. In particular, this says there are n + 1
gridded permutations of size n
on this tiling. We capture this idea by fusing the two columns into a single column.
>>> from tilings.strategies import FusionFactory
>>> strategy_generator = FusionFactory()
>>> for rule in strategy_generator(tiling):
... print(rule)
fuse columns 0 and 1
+-+-+ +-+
|\|\| ↣ |\|
+-+-+ +-+
\: Av(01) \: Av(01)
Crossing obstructions: Assumption 0:
01: (0, 0), (1, 0) can count points in cell (0, 0)
We use the symbol ↣
instead of =
to remind us that the counts of the two sides are definitely not the same. Notice, the right-hand side tiling here also now requires that we can count the number of points in cell (0, 0)
. If there are k
points in cell (0, 0)
in a gridded permutation then there will be k + 1
gridded permutations that fuse to this gridded permutation. Of course, here the number of points in cell(0, 0)
is going to be equal to the size of the gridded permutation, but in general, the points that need to be counted might not cover the whole tiling. For example, the following rule was used within specification to enumerate Av(123)
.
>>> tiling = Tiling(
... [
... GriddedPerm(Perm((0, 1)), ((0, 0), (0, 0))),
... GriddedPerm(Perm((0, 1)), ((0, 0), (1, 0))),
... GriddedPerm(Perm((0, 1)), ((1, 0), (1, 0))),
... GriddedPerm(Perm((0, 1, 2)), ((0, 0), (2, 0), (2, 0))),
... GriddedPerm(Perm((0, 1, 2)), ((1, 0), (2, 0), (2, 0))),
... GriddedPerm(Perm((0, 1, 2)), ((2, 0), (2, 0), (2, 0))),
... ]
... )
>>> for rule in strategy_generator(tiling):
... print(rule)
fuse columns 0 and 1
+-+-+-+ +-+-+
|\|\|1| ↣ |\|1|
+-+-+-+ +-+-+
1: Av(012) 1: Av(012)
\: Av(01) \: Av(01)
Crossing obstructions: Crossing obstructions:
01: (0, 0), (1, 0) 012: (0, 0), (1, 0), (1, 0)
012: (0, 0), (2, 0), (2, 0) Assumption 0:
012: (1, 0), (2, 0), (2, 0) can count points in cell (0, 0)
The TileScope algorithm can be resource-intensive in both time and memory. This codebase is fully compatible with PyPy, an alternative Python interpreter that usually runs TileScope 5x - 7x faster, at the cost of higher memory usage (sometimes as high as 2x). This extra memory usage is largely caused by PyPy's approach to incremental garbage collection, and as a result can be partially mitigated by setting the environmental variables described here. For example, the configuration
PYPY_GC_MAJOR_COLLECT=1.1
PYPY_GC_MAX_DELTA=200MB
PYPY_GC_INCREMENT_STEP=10GB
tends to improve memory usage at the cost of 30% - 50% extra time.
If memory usage, rather than time usage, is a bottleneck, then the default interpreter CPython
is preferred.
Finally, we'd like to reiterate, if you need support, have a suggestion, or just want to be up to date with the latest developments please join us on our Discord server where we'd be happy to hear from you!
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.