Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## Unreleased

### Added
- `CyclicPerm`, for cyclic permutations and cyclic pattern containment and avoidance. A cyclic permutation is stored by its rotation that starts with its minimum.
- `RotationallyStableStrategy`, a fast enumeration strategy for a class whose basis, or a symmetry of it, is closed under rotation (cyclic shifts). It counts the class through its min cut, following the cuts as long as they are rotationally stable.

### Fixed
- Fixed bug in `Perm.rtlmax_ltrmin_decomposition` and fixed associated tests.

Expand Down
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,29 @@ given class.
... print(strat.reference())
Enumeration of Permutation Classes and Weighted Labelled Independent Sets: Corollary 4.6
The class contains only finitely many simple permutations
>>> basis = [Perm((0, 1, 2, 3)), Perm((1, 2, 3, 0)), Perm((2, 3, 0, 1)), Perm((3, 0, 1, 2))]
>>> for strat in find_strategies(basis):
... print(strat.reference())
The class is closed under rotation (cyclic shifts): one in n of its permutations of length n starts with the minimum, and the min cut counts these

The output is the name of a paper, followed by the statement in the paper where the enumeration strategy is discussed or stated.

The last basis is closed under rotation, the cyclic shift that moves the first entry
of each pattern to the end. Its min cut, the basis of the permutations p for which
1 + p is in the class, may be closed under rotation again, and the strategy follows
the cuts as far as they go.

.. code-block:: python

>>> from permuta.enumeration_strategies.rotationally_stable import (
... RotationallyStableStrategy,
... )
>>> strategy = RotationallyStableStrategy(basis)
>>> [str(b) for b in strategy.chain()]
['{0123, 1230, 2301, 3012}', '{012, 2301}']
>>> [strategy.count(n) for n in range(9)]
[1, 1, 2, 6, 20, 65, 198, 560, 1480]

Permutation statistics
######################

Expand Down
2 changes: 2 additions & 0 deletions permuta/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .circular import CyclicPerm
from .patterns import BivincularPatt, CovincularPatt, MeshPatt, Perm, VincularPatt
from .perm_sets.permset import Av, Basis, MeshBasis

Expand All @@ -12,4 +13,5 @@
"BivincularPatt",
"CovincularPatt",
"VincularPatt",
"CyclicPerm",
]
3 changes: 3 additions & 0 deletions permuta/circular/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cyclic_perm import CyclicPerm

__all__ = ("CyclicPerm",)
214 changes: 214 additions & 0 deletions permuta/circular/cyclic_perm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import itertools
from typing import Iterable, Iterator, Tuple

from permuta.patterns.perm import Perm

__all__ = ("CyclicPerm",)


class CyclicPerm:
"""A cyclic permutation: the equivalence class of all rotations of a
linear permutation, stored via its unique ``1 ⊕ σ`` representative
(the rotation that places the smallest element first).
"""

__slots__ = ("_rep",)
_rep: Perm # set in __new__ and _from_canonical; declared for type checkers

def __new__(cls, iterable: Iterable[int] = ()) -> "CyclicPerm":
"""Build a ``CyclicPerm`` from any rotation of the underlying class.

Examples:
>>> CyclicPerm((3, 1, 2, 4, 0))
CyclicPerm((0, 3, 1, 2, 4))
>>> CyclicPerm((0, 3, 1, 2, 4)) == CyclicPerm((4, 0, 3, 1, 2))
True
>>> CyclicPerm(())
CyclicPerm(())
"""
obj = object.__new__(cls)
p = iterable if isinstance(iterable, Perm) else Perm(iterable)
obj._rep = CyclicPerm._canonical(p)
return obj

@staticmethod
def _canonical(p: Perm) -> Perm:
if len(p) == 0:
return p
return p.shift_left(p.index(0))

@classmethod
def _from_canonical(cls, p: Perm) -> "CyclicPerm":
"""Fast path when ``p`` already starts with ``0``."""
obj = object.__new__(cls)
obj._rep = p
return obj

@classmethod
def from_perm(cls, p: Perm) -> "CyclicPerm":
"""Build the cyclic class of a ``Perm``."""
return cls(p)

@classmethod
def to_standard(cls, iterable: Iterable) -> "CyclicPerm":
"""Build a cyclic permutation by standardizing any iterable of distinct
comparable elements (1-indexed inputs work fine).

Examples:
>>> CyclicPerm.to_standard("42351")
CyclicPerm((0, 3, 1, 2, 4))
"""
return cls(Perm.to_standard(iterable))

@classmethod
def of_length(cls, n: int) -> Iterator["CyclicPerm"]:
"""Yield every cyclic class of length ``n`` exactly once.

Examples:
>>> sorted(str(c) for c in CyclicPerm.of_length(3))
['[012]', '[021]']
>>> sum(1 for _ in CyclicPerm.of_length(5))
24
"""
if n == 0:
yield cls._from_canonical(Perm(()))
return
if n == 1:
yield cls._from_canonical(Perm((0,)))
return
for sigma in Perm.of_length(n - 1):
yield cls._from_canonical(Perm((0,) + tuple(v + 1 for v in sigma)))

def representative(self) -> Perm:
"""The canonical ``1 ⊕ σ`` linear representative (starts with 0)."""
return self._rep

def sigma(self) -> Perm:
"""The ``σ`` in ``1 ⊕ σ`` as a ``Perm`` of length ``n - 1``."""
if len(self._rep) <= 1:
return Perm(())
return Perm(v - 1 for v in itertools.islice(self._rep, 1, None))

def rotations(self) -> Iterator[Perm]:
"""All ``n`` linear rotations of the representative."""
rep = self._rep
n = len(rep)
for k in range(n):
yield rep.shift_left(k)

def reverse(self) -> "CyclicPerm":
"""The cyclic class of the reverse of the representative."""
return CyclicPerm(self._rep.reverse())

def complement(self) -> "CyclicPerm":
"""The cyclic class of the complement of the representative."""
return CyclicPerm(self._rep.complement())

def reverse_complement(self) -> "CyclicPerm":
"""The cyclic class of the reverse-complement of the representative."""
return CyclicPerm(self._rep.reverse_complement())

def contains(self, *patts: "CyclicPerm") -> bool:
"""Cyclic containment.

Examples:
>>> sigma = CyclicPerm((3, 1, 2, 4, 0))
>>> sigma.contains(CyclicPerm((0, 1, 2, 3)))
True
>>> Perm((3, 1, 2, 4, 0)).avoids(Perm((0, 1, 2, 3)))
True
"""
return all(self._contains_one(p) for p in patts)

def avoids(self, *patts: "CyclicPerm") -> bool:
"""Cyclic avoidance. True iff no cyclic pattern in ``patts`` occurs."""
return all(not self._contains_one(p) for p in patts)

def avoids_set(self, patts: Iterable["CyclicPerm"]) -> bool:
"""Cyclic avoidance of every cyclic pattern in the iterable ``patts``."""
return self.avoids(*tuple(patts))

def _contains_one(self, patt: "CyclicPerm") -> bool:
rep = self._rep
if len(patt) > len(rep):
return False
if len(patt) == 0:
return True
return any(rep.contains(rot) for rot in patt.rotations())

def occurrences_of(
self, patt: "CyclicPerm"
) -> Iterator[Tuple[int, Tuple[int, ...]]]:
"""Yield ``(rotation_index, positions)`` for each linear copy of a
rotation of ``patt`` inside the canonical representative.
"""
rep = self._rep
if len(patt) > len(rep):
return
if len(patt) == 0:
yield (0, ())
return
for k, rot in enumerate(patt.rotations()):
for positions in rot.occurrences_in(rep):
yield (k, positions)

def count_occurrences_of(self, patt: "CyclicPerm") -> int:
"""The number of occurrences that ``occurrences_of`` yields."""
return sum(1 for _ in self.occurrences_of(patt))

def cyclic_descents(self) -> Iterator[int]:
"""Yield indices ``i`` with ``π_i > π_{i+1 mod n}``."""
rep = self._rep
n = len(rep)
for i in range(n):
if rep[i] > rep[(i + 1) % n]:
yield i

def cdes(self) -> int:
"""The cyclic descent number.

Examples:
>>> CyclicPerm.to_standard("23514").cdes()
2
"""
return sum(1 for _ in self.cyclic_descents())

def __len__(self) -> int:
return len(self._rep)

def __iter__(self) -> Iterator[int]:
return iter(self._rep)

def __eq__(self, other: object) -> bool:
if isinstance(other, CyclicPerm):
return self._rep == other._rep
return NotImplemented

def __hash__(self) -> int:
return hash(("CyclicPerm", self._rep))

def __lt__(self, other: "CyclicPerm") -> bool:
if not isinstance(other, CyclicPerm):
return NotImplemented
return (len(self._rep), tuple(self._rep)) < (len(other._rep), tuple(other._rep))

def __le__(self, other: "CyclicPerm") -> bool:
if not isinstance(other, CyclicPerm):
return NotImplemented
return (len(self._rep), tuple(self._rep)) <= (
len(other._rep),
tuple(other._rep),
)

def __contains__(self, patt: "CyclicPerm") -> bool:
return self._contains_one(patt)

def __repr__(self) -> str:
return f"CyclicPerm({tuple(self._rep)!r})"

def __str__(self) -> str:
if not self._rep:
return "[ε]"
if len(self._rep) <= 10:
return "[" + "".join(str(i) for i in self._rep) + "]"
return "[" + ",".join(str(i) for i in self._rep) + "]"
4 changes: 3 additions & 1 deletion permuta/enumeration_strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from .core_strategies import core_strategies
from .finitely_many_simples import FinitelyManySimplesStrategy
from .insertion_encodable import InsertionEncodingStrategy
from .rotationally_stable import RotationallyStableStrategy

fast_enumeration_strategies: List[Type[EnumerationStrategy]] = [
InsertionEncodingStrategy
InsertionEncodingStrategy,
RotationallyStableStrategy,
]
fast_enumeration_strategies.extend(core_strategies)

Expand Down
Loading
Loading