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 [email protected] (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.