Skip to content

Commit 3d6e3f6

Browse files
Claudeclaude
andcommitted
Add CyclicPerm for cyclic pattern containment and avoidance
Implements the rotation-equivalence-class model from Domagalski et al., "Cyclic Pattern Containment and Avoidance" (arXiv:2106.02534). Cyclic permutations are stored via their unique "1 + sigma" canonical representative (the rotation that places the smallest element first), so cyclic containment [sigma] contains [p] reduces to checking whether the canonical rep of sigma linearly contains any rotation of p. Tests verify: Callan's single-pattern enumeration for S_4, several three-pattern results from section 4, the cyclic Erdos-Szekeres bound, trivial Wilf equivalences, cdes rotation invariance, and the paper's opening example that [42351] contains [1234] even though 42351 avoids it linearly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2e59ec4 commit 3d6e3f6

5 files changed

Lines changed: 511 additions & 0 deletions

File tree

permuta/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .circular import CyclicPerm
12
from .patterns import BivincularPatt, CovincularPatt, MeshPatt, Perm, VincularPatt
23
from .perm_sets.permset import Av, Basis, MeshBasis
34

@@ -12,4 +13,5 @@
1213
"BivincularPatt",
1314
"CovincularPatt",
1415
"VincularPatt",
16+
"CyclicPerm",
1517
]

permuta/circular/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cyclic_perm import CyclicPerm
2+
3+
__all__ = ("CyclicPerm",)

permuta/circular/cyclic_perm.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
from typing import Iterable, Iterator, Tuple
2+
3+
from permuta.patterns.perm import Perm
4+
5+
__all__ = ("CyclicPerm",)
6+
7+
8+
class CyclicPerm:
9+
"""A cyclic permutation: the equivalence class of all rotations of a
10+
linear permutation, stored via its unique ``1 ⊕ σ`` representative
11+
(the rotation that places the smallest element first).
12+
"""
13+
14+
__slots__ = ("_rep",)
15+
16+
def __new__(cls, iterable: Iterable[int] = ()) -> "CyclicPerm":
17+
"""Build a ``CyclicPerm`` from any rotation of the underlying class.
18+
19+
Examples:
20+
>>> CyclicPerm((3, 1, 2, 4, 0))
21+
CyclicPerm((0, 3, 1, 2, 4))
22+
>>> CyclicPerm((0, 3, 1, 2, 4)) == CyclicPerm((4, 0, 3, 1, 2))
23+
True
24+
>>> CyclicPerm(())
25+
CyclicPerm(())
26+
"""
27+
obj = object.__new__(cls)
28+
p = iterable if isinstance(iterable, Perm) else Perm(iterable)
29+
obj._rep = CyclicPerm._canonical(p)
30+
return obj
31+
32+
@staticmethod
33+
def _canonical(p: Perm) -> Perm:
34+
if len(p) == 0:
35+
return p
36+
return p.shift_left(p.index(0))
37+
38+
@classmethod
39+
def _from_canonical(cls, p: Perm) -> "CyclicPerm":
40+
"""Fast path when ``p`` already starts with ``0``."""
41+
obj = object.__new__(cls)
42+
obj._rep = p
43+
return obj
44+
45+
@classmethod
46+
def from_perm(cls, p: Perm) -> "CyclicPerm":
47+
"""Build the cyclic class of a ``Perm``."""
48+
return cls(p)
49+
50+
@classmethod
51+
def to_standard(cls, iterable: Iterable) -> "CyclicPerm":
52+
"""Build a cyclic permutation by standardizing any iterable of distinct
53+
comparable elements (1-indexed inputs work fine).
54+
55+
Examples:
56+
>>> CyclicPerm.to_standard("42351")
57+
CyclicPerm((0, 3, 1, 2, 4))
58+
"""
59+
return cls(Perm.to_standard(iterable))
60+
61+
@classmethod
62+
def of_length(cls, n: int) -> Iterator["CyclicPerm"]:
63+
"""Yield every cyclic class of length ``n`` exactly once.
64+
65+
Examples:
66+
>>> sorted(str(c) for c in CyclicPerm.of_length(3))
67+
['[012]', '[021]']
68+
>>> sum(1 for _ in CyclicPerm.of_length(5))
69+
24
70+
"""
71+
if n == 0:
72+
yield cls._from_canonical(Perm(()))
73+
return
74+
if n == 1:
75+
yield cls._from_canonical(Perm((0,)))
76+
return
77+
for sigma in Perm.of_length(n - 1):
78+
yield cls._from_canonical(Perm((0,) + tuple(v + 1 for v in sigma)))
79+
80+
def representative(self) -> Perm:
81+
"""The canonical ``1 ⊕ σ`` linear representative (starts with 0)."""
82+
return self._rep
83+
84+
def sigma(self) -> Perm:
85+
"""The ``σ`` in ``1 ⊕ σ`` as a ``Perm`` of length ``n - 1``."""
86+
if len(self._rep) <= 1:
87+
return Perm(())
88+
return Perm(v - 1 for v in self._rep[1:])
89+
90+
def rotations(self) -> Iterator[Perm]:
91+
"""All ``n`` linear rotations of the representative."""
92+
rep = self._rep
93+
n = len(rep)
94+
for k in range(n):
95+
yield rep.shift_left(k)
96+
97+
def reverse(self) -> "CyclicPerm":
98+
return CyclicPerm(self._rep.reverse())
99+
100+
def complement(self) -> "CyclicPerm":
101+
return CyclicPerm(self._rep.complement())
102+
103+
def reverse_complement(self) -> "CyclicPerm":
104+
return CyclicPerm(self._rep.reverse_complement())
105+
106+
def contains(self, *patts: "CyclicPerm") -> bool:
107+
"""Cyclic containment.
108+
109+
Examples:
110+
>>> sigma = CyclicPerm((3, 1, 2, 4, 0))
111+
>>> sigma.contains(CyclicPerm((0, 1, 2, 3)))
112+
True
113+
>>> Perm((3, 1, 2, 4, 0)).avoids(Perm((0, 1, 2, 3)))
114+
True
115+
"""
116+
return all(self._contains_one(p) for p in patts)
117+
118+
def avoids(self, *patts: "CyclicPerm") -> bool:
119+
"""Cyclic avoidance. True iff no cyclic pattern in ``patts`` occurs."""
120+
return all(not self._contains_one(p) for p in patts)
121+
122+
def avoids_set(self, patts: Iterable["CyclicPerm"]) -> bool:
123+
return self.avoids(*tuple(patts))
124+
125+
def _contains_one(self, patt: "CyclicPerm") -> bool:
126+
rep = self._rep
127+
if len(patt) > len(rep):
128+
return False
129+
if len(patt) == 0:
130+
return True
131+
return any(rep.contains(rot) for rot in patt.rotations())
132+
133+
def occurrences_of(
134+
self, patt: "CyclicPerm"
135+
) -> Iterator[Tuple[int, Tuple[int, ...]]]:
136+
"""Yield ``(rotation_index, positions)`` for each linear copy of a
137+
rotation of ``patt`` inside the canonical representative.
138+
"""
139+
rep = self._rep
140+
if len(patt) > len(rep):
141+
return
142+
if len(patt) == 0:
143+
yield (0, ())
144+
return
145+
for k, rot in enumerate(patt.rotations()):
146+
for positions in rot.occurrences_in(rep):
147+
yield (k, positions)
148+
149+
def count_occurrences_of(self, patt: "CyclicPerm") -> int:
150+
return sum(1 for _ in self.occurrences_of(patt))
151+
152+
def cyclic_descents(self) -> Iterator[int]:
153+
"""Yield indices ``i`` with ``π_i > π_{i+1 mod n}``."""
154+
rep = self._rep
155+
n = len(rep)
156+
for i in range(n):
157+
if rep[i] > rep[(i + 1) % n]:
158+
yield i
159+
160+
def cdes(self) -> int:
161+
"""The cyclic descent number.
162+
163+
Examples:
164+
>>> CyclicPerm.to_standard("23514").cdes()
165+
2
166+
"""
167+
return sum(1 for _ in self.cyclic_descents())
168+
169+
def __len__(self) -> int:
170+
return len(self._rep)
171+
172+
def __iter__(self) -> Iterator[int]:
173+
return iter(self._rep)
174+
175+
def __eq__(self, other: object) -> bool:
176+
if isinstance(other, CyclicPerm):
177+
return self._rep == other._rep
178+
return NotImplemented
179+
180+
def __hash__(self) -> int:
181+
return hash(("CyclicPerm", self._rep))
182+
183+
def __lt__(self, other: "CyclicPerm") -> bool:
184+
if not isinstance(other, CyclicPerm):
185+
return NotImplemented
186+
return (len(self._rep), tuple(self._rep)) < (len(other._rep), tuple(other._rep))
187+
188+
def __le__(self, other: "CyclicPerm") -> bool:
189+
if not isinstance(other, CyclicPerm):
190+
return NotImplemented
191+
return (len(self._rep), tuple(self._rep)) <= (
192+
len(other._rep),
193+
tuple(other._rep),
194+
)
195+
196+
def __contains__(self, patt: "CyclicPerm") -> bool:
197+
return self._contains_one(patt)
198+
199+
def __repr__(self) -> str:
200+
return f"CyclicPerm({tuple(self._rep)!r})"
201+
202+
def __str__(self) -> str:
203+
if not self._rep:
204+
return "[ε]"
205+
if len(self._rep) <= 10:
206+
return "[" + "".join(str(i) for i in self._rep) + "]"
207+
return "[" + ",".join(str(i) for i in self._rep) + "]"

tests/circular/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)