Skip to content

Commit 6d4b2de

Browse files
committed
adds right and above juxtaposition functionality and unit tests
1 parent 2e59ec4 commit 6d4b2de

2 files changed

Lines changed: 282 additions & 2 deletions

File tree

permuta/perm_sets/permset.py

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import multiprocessing
2-
from itertools import islice
3-
from typing import ClassVar, Dict, Iterable, List, NamedTuple, Optional, Union
2+
from itertools import combinations, islice
3+
from typing import ClassVar, Dict, Iterable, Iterator, List, NamedTuple, Optional, Union
44

55
from ..patterns import MeshPatt, Perm
66
from ..permutils import is_finite, is_insertion_encodable, is_polynomial
@@ -202,6 +202,130 @@ def _all(self) -> Iterable[Perm]:
202202
yield from gen
203203
length += 1
204204

205+
def right_juxtaposition(self, other: "Av") -> "Av":
206+
"""Compute the basis of the juxtaposition of two permutation classes.
207+
208+
Given self = Av(B1) and other = Av(B2), returns the permutation class
209+
E = Av(B) where E consists of all permutations that can be written as
210+
the juxtaposition of a permutation from self on the left and a
211+
permutation from other on the right.
212+
213+
Raises NotImplementedError: If either basis is a MeshBasis.
214+
"""
215+
if not isinstance(self.basis, Basis) or not isinstance(other.basis, Basis):
216+
raise NotImplementedError(Av._BASIS_ONLY_MSG)
217+
218+
candidates: List[Perm] = []
219+
220+
for b1 in self.basis:
221+
for b2 in other.basis:
222+
# |σ| = 0 case: no overlap
223+
candidates.extend(self._sigma_0_candidates(b1, b2))
224+
# |σ| = 1 case: one element overlap
225+
candidates.extend(self._sigma_1_candidates(b1, b2))
226+
227+
# Basis constructor automatically minimizes
228+
return Av(Basis(*candidates))
229+
230+
def above_juxtaposition(self, other: "Av") -> "Av":
231+
"""Compute the basis of the above juxtaposition of two permutation classes.
232+
233+
Given self = Av(B1) and other = Av(B2), returns the permutation class
234+
where self is on the bottom and other is on top.
235+
236+
This is computed by taking inverses, computing right_juxtaposition,
237+
then inverting the result.
238+
239+
Raises NotImplementedError: If either basis is a MeshBasis.
240+
"""
241+
if not isinstance(self.basis, Basis) or not isinstance(other.basis, Basis):
242+
raise NotImplementedError(Av._BASIS_ONLY_MSG)
243+
244+
# Compute inverse classes
245+
self_inverse = Av(Basis(*[p.inverse() for p in self.basis]))
246+
other_inverse = Av(Basis(*[p.inverse() for p in other.basis]))
247+
248+
# Compute right juxtaposition of inverses
249+
result_inverse = self_inverse.right_juxtaposition(other_inverse)
250+
251+
# Return inverse of result
252+
return Av(Basis(*[p.inverse() for p in result_inverse.basis]))
253+
254+
@staticmethod
255+
def _sigma_0_candidates(b1: Perm, b2: Perm) -> Iterator[Perm]:
256+
"""Generate candidates where left and right patterns don't overlap.
257+
258+
Generates all permutations of length |b1| + |b2| where the first |b1|
259+
positions have pattern b1 and the last |b2| positions have pattern b2.
260+
"""
261+
n1, n2 = len(b1), len(b2)
262+
total = n1 + n2
263+
264+
# Choose which values go to the left block
265+
for left_values in combinations(range(total), n1):
266+
right_values = [v for v in range(total) if v not in left_values]
267+
268+
# Build the permutation
269+
result = [0] * total
270+
# Left positions get values according to pattern b1
271+
for pos in range(n1):
272+
result[pos] = left_values[b1[pos]]
273+
# Right positions get values according to pattern b2
274+
for pos in range(n2):
275+
result[n1 + pos] = right_values[b2[pos]]
276+
277+
yield Perm(result)
278+
279+
@staticmethod
280+
def _sigma_1_candidates(b1: Perm, b2: Perm) -> Iterator[Perm]:
281+
"""Generate candidates where left and right patterns overlap by one element.
282+
283+
Generates all permutations of length |b1| + |b2| - 1 where the first |b1|
284+
positions have pattern b1 and the last |b2| positions have pattern b2,
285+
with position |b1| - 1 shared between both patterns.
286+
"""
287+
n1, n2 = len(b1), len(b2)
288+
total = n1 + n2 - 1
289+
290+
# The shared position is at index n1 - 1
291+
# Its value v must satisfy: v = b1[-1] + b2[0]
292+
# (it must be at rank b1[-1] among left values and rank b2[0] among right values)
293+
v = b1[-1] + b2[0]
294+
295+
# Values less than v: {0, ..., v-1}
296+
# Values greater than v: {v+1, ..., total-1}
297+
values_below = list(range(v))
298+
values_above = list(range(v + 1, total))
299+
300+
# Left block needs b1[-1] values below v, right block gets the rest
301+
k1 = b1[-1] # number of values < v in left block
302+
303+
# Iterate over all ways to partition values below v
304+
for left_below in combinations(values_below, k1):
305+
right_below = [x for x in values_below if x not in left_below]
306+
307+
# Iterate over all ways to partition values above v
308+
for left_above in combinations(values_above, n1 - 1 - k1):
309+
right_above = [x for x in values_above if x not in left_above]
310+
311+
# Build the left and right value sets
312+
left_values = sorted(list(left_below) + [v] + list(left_above))
313+
right_values = sorted(right_below + [v] + right_above)
314+
315+
# Build the permutation
316+
result = [0] * total
317+
318+
# Left positions (0 to n1-1) get values according to pattern b1
319+
for pos in range(n1):
320+
result[pos] = left_values[b1[pos]]
321+
322+
# Right positions (n1-1 to total-1) get values according to pattern b2
323+
# But position n1-1 is already set, so we only set n1 to total-1
324+
for pos in range(1, n2):
325+
result[n1 - 1 + pos] = right_values[b2[pos]]
326+
327+
yield Perm(result)
328+
205329
def __str__(self) -> str:
206330
return f"Av({','.join(str(p) for p in self.basis)})"
207331

tests/perm_sets/test_av.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,159 @@ def test_invalid_ops_with_mesh_patt():
291291
Av(MeshBasis(Perm((0, 1)))).is_insertion_encodable()
292292
with pytest.raises(NotImplementedError):
293293
Av(MeshBasis(Perm((0, 1)))).is_polynomial()
294+
295+
296+
# Tests for right_juxtaposition
297+
298+
299+
def test_right_juxtaposition_basic():
300+
"""Test Av(21) | Av(12) = Av(213, 312)."""
301+
av_21 = Av(Basis(Perm((1, 0))))
302+
av_12 = Av(Basis(Perm((0, 1))))
303+
result = av_21.right_juxtaposition(av_12)
304+
expected_basis = {Perm((1, 0, 2)), Perm((2, 0, 1))}
305+
assert set(result.basis) == expected_basis
306+
307+
308+
def test_right_juxtaposition_same_class():
309+
"""Test Av(21) | Av(21) gives expected basis."""
310+
av_21 = Av(Basis(Perm((1, 0))))
311+
result = av_21.right_juxtaposition(av_21)
312+
# Basis should be {321, 2143, 2431} = {(2,1,0), (1,0,3,2), (1,3,2,0)}
313+
expected_basis = {Perm((2, 1, 0)), Perm((1, 0, 3, 2)), Perm((2, 0, 3, 1))}
314+
assert set(result.basis) == expected_basis
315+
316+
317+
def test_right_juxtaposition_enumeration():
318+
"""Test that juxtaposition class has correct enumeration."""
319+
av_21 = Av(Basis(Perm((1, 0))))
320+
av_12 = Av(Basis(Perm((0, 1))))
321+
result = av_21.right_juxtaposition(av_12)
322+
# [Av(21)|Av(12)] = permutations that can be split into decreasing|increasing
323+
# Enumeration: 1, 1, 2, 4, 8, 16, 32 (powers of 2 starting at n=2)
324+
assert result.enumeration(6) == [1, 1, 2, 4, 8, 16, 32]
325+
326+
327+
def test_right_juxtaposition_multiple_basis_elements():
328+
"""Test juxtaposition with multiple basis elements."""
329+
av_21_12 = Av(Basis(Perm((1, 0)), Perm((0, 1)))) # Only contains empty and singleton
330+
av_132 = Av(Basis(Perm((0, 2, 1))))
331+
result = av_21_12.right_juxtaposition(av_132)
332+
# The result should be a valid Av object with a minimized basis
333+
assert isinstance(result.basis, Basis)
334+
assert len(result.basis) > 0
335+
336+
337+
def test_right_juxtaposition_longer_patterns():
338+
"""Test juxtaposition with longer patterns."""
339+
av_132 = Av(Basis(Perm((0, 2, 1))))
340+
av_231 = Av(Basis(Perm((1, 2, 0))))
341+
result = av_132.right_juxtaposition(av_231)
342+
# Verify result is valid and has expected structure
343+
assert isinstance(result.basis, Basis)
344+
# All basis elements should have length between 3 and 6 (|b1|+|b2|-1 to |b1|+|b2|)
345+
for perm in result.basis:
346+
assert 5 <= len(perm) <= 6
347+
348+
349+
def test_right_juxtaposition_mesh_basis_raises():
350+
"""Test that juxtaposition with MeshBasis raises NotImplementedError."""
351+
av_classical = Av(Basis(Perm((1, 0))))
352+
av_mesh = Av(MeshBasis(Perm((0, 1))))
353+
with pytest.raises(NotImplementedError):
354+
av_classical.right_juxtaposition(av_mesh)
355+
with pytest.raises(NotImplementedError):
356+
av_mesh.right_juxtaposition(av_classical)
357+
358+
359+
def test_right_juxtaposition_containment():
360+
"""Test that permutations in the juxtaposition class can be split correctly."""
361+
av_21 = Av(Basis(Perm((1, 0))))
362+
av_12 = Av(Basis(Perm((0, 1))))
363+
result = av_21.right_juxtaposition(av_12)
364+
365+
# Check some permutations that should be in the class
366+
# 21 can be split as (2)|(1) where (2) is decreasing and (1) is increasing
367+
assert Perm((1, 0)) in result
368+
# 12 can be split as ()|(12) where () is trivially decreasing and (12) is increasing
369+
assert Perm((0, 1)) in result
370+
# 1 is trivially in the class
371+
assert Perm((0,)) in result
372+
373+
# Check some permutations that should NOT be in the class
374+
# 213 = (1,0,2) is a basis element, so not in the class
375+
assert Perm((1, 0, 2)) not in result
376+
# 312 = (2,0,1) is a basis element, so not in the class
377+
assert Perm((2, 0, 1)) not in result
378+
379+
380+
# Tests for above_juxtaposition
381+
382+
383+
def test_above_juxtaposition_basic():
384+
"""Test basic above juxtaposition with Av(21) below and Av(12) above."""
385+
av_21 = Av(Basis(Perm((1, 0))))
386+
av_12 = Av(Basis(Perm((0, 1))))
387+
result = av_21.above_juxtaposition(av_12)
388+
# Result should be valid Av with Basis
389+
assert isinstance(result.basis, Basis)
390+
assert len(result.basis) > 0
391+
392+
393+
def test_above_juxtaposition_inverse_relationship():
394+
"""Test that above_juxtaposition relates to right_juxtaposition via inverses."""
395+
av_21 = Av(Basis(Perm((1, 0))))
396+
av_132 = Av(Basis(Perm((0, 2, 1))))
397+
398+
# Compute above juxtaposition directly
399+
above_result = av_21.above_juxtaposition(av_132)
400+
401+
# Compute via inverses manually
402+
av_21_inv = Av(Basis(*[p.inverse() for p in av_21.basis]))
403+
av_132_inv = Av(Basis(*[p.inverse() for p in av_132.basis]))
404+
right_result = av_21_inv.right_juxtaposition(av_132_inv)
405+
manual_result = Av(Basis(*[p.inverse() for p in right_result.basis]))
406+
407+
# The bases should be equivalent
408+
assert set(above_result.basis) == set(manual_result.basis)
409+
410+
411+
def test_above_juxtaposition_enumeration():
412+
"""Test that above juxtaposition class has expected enumeration."""
413+
av_21 = Av(Basis(Perm((1, 0))))
414+
av_12 = Av(Basis(Perm((0, 1))))
415+
result = av_21.above_juxtaposition(av_12)
416+
# Permutations that can be split by value: lower values decreasing, upper increasing
417+
# This should give 2^(n-1) for n >= 1
418+
assert result.enumeration(6) == [1, 1, 2, 4, 8, 16, 32]
419+
420+
421+
def test_above_juxtaposition_same_class():
422+
"""Test above juxtaposition with the same class."""
423+
av_21 = Av(Basis(Perm((1, 0))))
424+
result = av_21.above_juxtaposition(av_21)
425+
# Should be valid and have a non-empty basis
426+
assert isinstance(result.basis, Basis)
427+
assert len(result.basis) > 0
428+
429+
430+
def test_above_juxtaposition_longer_patterns():
431+
"""Test above juxtaposition with longer patterns."""
432+
av_132 = Av(Basis(Perm((0, 2, 1))))
433+
av_231 = Av(Basis(Perm((1, 2, 0))))
434+
result = av_132.above_juxtaposition(av_231)
435+
# Verify result is valid
436+
assert isinstance(result.basis, Basis)
437+
# All basis elements should have length between 5 and 6
438+
for perm in result.basis:
439+
assert 5 <= len(perm) <= 6
440+
441+
442+
def test_above_juxtaposition_mesh_basis_raises():
443+
"""Test that above_juxtaposition with MeshBasis raises NotImplementedError."""
444+
av_classical = Av(Basis(Perm((1, 0))))
445+
av_mesh = Av(MeshBasis(Perm((0, 1))))
446+
with pytest.raises(NotImplementedError):
447+
av_classical.above_juxtaposition(av_mesh)
448+
with pytest.raises(NotImplementedError):
449+
av_mesh.above_juxtaposition(av_classical)

0 commit comments

Comments
 (0)