From 5e938501e18b2120473036ef24313c8cc2795520 Mon Sep 17 00:00:00 2001 From: Soumyadip Sarkar Date: Sat, 25 Apr 2026 13:05:05 +0530 Subject: [PATCH 1/2] Add permutation parity analysis helpers --- src/tensor_layouts/analysis.py | 39 +++++++++++++++++++ tests/analysis.py | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/tensor_layouts/analysis.py b/src/tensor_layouts/analysis.py index 8e216f8..098089f 100644 --- a/src/tensor_layouts/analysis.py +++ b/src/tensor_layouts/analysis.py @@ -57,6 +57,8 @@ "cycles", "fixed_points", "order", + "permutation_parity", + "is_even_permutation", "contiguity", "mode_contiguity", "slice_contiguity", @@ -1046,6 +1048,43 @@ def order(layout: LayoutExpr) -> int: return result +def permutation_parity(layout: LayoutExpr) -> int: + """Return permutation parity of a dense, injective layout (+1 even, -1 odd). + + The layout is interpreted as a permutation on ``[0, size(layout))`` after + rebasing its dense image interval (same normalization used by + :func:`cycles` and :func:`order`). + + Returns: + +1 for an even permutation, -1 for an odd permutation. + Empty layouts are treated as the identity permutation and return +1. + + Raises: + ValueError: if layout is not injective or its image has gaps. + + Examples: + permutation_parity(Layout(4, 1)) # +1 (identity) + + # Adjacent swap is odd + permutation_parity(Layout((2, 2), (2, 1))) # -1 + """ + cycle_list = cycles(layout) + # A k-cycle is equivalent to (k - 1) transpositions. + transposition_count = sum(len(cycle) - 1 for cycle in cycle_list) + return 1 if transposition_count % 2 == 0 else -1 + + +def is_even_permutation(layout: LayoutExpr) -> bool: + """Return True if the induced dense permutation is even. + + Equivalent to ``permutation_parity(layout) == +1``. + + Raises ValueError under the same conditions as + :func:`permutation_parity`. + """ + return permutation_parity(layout) == 1 + + # ============================================================================= # Contiguity # ============================================================================= diff --git a/tests/analysis.py b/tests/analysis.py index 80212ce..8ed758a 100644 --- a/tests/analysis.py +++ b/tests/analysis.py @@ -596,6 +596,76 @@ def test_order_negative_stride_rebases_dense_image(): assert order(Layout(4, -1)) == 2 +## permutation parity + + +def test_permutation_parity_identity_even(): + """Identity mapping is an even permutation.""" + assert permutation_parity(Layout(8, 1)) == 1 + assert is_even_permutation(Layout(8, 1)) is True + + +def test_permutation_parity_empty_layout_is_identity(): + """Empty layout has identity parity by convention.""" + assert permutation_parity(Layout(0, 1)) == 1 + assert is_even_permutation(Layout(0, 1)) is True + + +def test_permutation_parity_single_swap_odd(): + """A single transposition has odd parity.""" + # Row-major 2x2 induces [0, 2, 1, 3], i.e. swap(1, 2). + lyt = Layout((2, 2), (2, 1)) + assert permutation_parity(lyt) == -1 + assert is_even_permutation(lyt) is False + + +def test_permutation_parity_two_disjoint_swaps_even(): + """Two disjoint transpositions compose to an even permutation.""" + # XOR with bit 1 on 8 elements yields (2 3)(6 7). + lyt = compose(Swizzle(1, 0, 1), Layout(8, 1)) + assert permutation_parity(lyt) == 1 + assert is_even_permutation(lyt) is True + + +def test_permutation_parity_negative_dense_reverse_even(): + """Dense reverse on 4 elements is two swaps, hence even.""" + assert permutation_parity(Layout(4, -1)) == 1 + assert is_even_permutation(Layout(4, -1)) is True + + +def test_permutation_parity_matches_inversion_count(): + """Parity must match independent inversion-count computation.""" + candidates = [ + Layout(8, 1), # identity + Layout((2, 2), (2, 1)), # one swap (odd) + Layout(4, -1), # reverse 4 (even) + compose(Swizzle(1, 0, 1), Layout(8, 1)), # two disjoint swaps (even) + compose(Swizzle(2, 0, 1), Layout(8, 1)), # non-trivial dense permutation + ] + + for lyt in candidates: + values = [lyt(i) for i in range(size(lyt))] + base = min(values) if values else 0 + perm = [v - base for v in values] + inversions = 0 + for i in range(len(perm)): + for j in range(i + 1, len(perm)): + if perm[i] > perm[j]: + inversions += 1 + expected = 1 if inversions % 2 == 0 else -1 + assert permutation_parity(lyt) == expected + assert is_even_permutation(lyt) is (expected == 1) + + +def test_permutation_parity_requires_dense_injective_layout(): + """Parity analysis rejects layouts with aliasing or gaps.""" + with pytest.raises(ValueError, match="not injective"): + permutation_parity(Layout((4, 2), (0, 1))) + + with pytest.raises(ValueError, match="not a dense interval"): + permutation_parity(Layout(4, 2)) + + ## contiguity From 91cc5e5a8c9175d4283da939c0a90b4f5720a8dd Mon Sep 17 00:00:00 2001 From: Soumyadip Sarkar Date: Sat, 25 Apr 2026 13:29:01 +0530 Subject: [PATCH 2/2] Document permutation parity APIs in analysis docs --- docs/analysis_api.md | 45 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/analysis_api.md b/docs/analysis_api.md index 2158347..2035726 100644 --- a/docs/analysis_api.md +++ b/docs/analysis_api.md @@ -46,7 +46,8 @@ bijective layouts, and trace the algebra step by step. from tensor_layouts.analysis import ( image, is_injective, is_surjective, is_bijective, offset_table, footprint, gap_profile, bank_conflicts, coalescing_efficiency, - cycles, fixed_points, order, thread_stride_profile, explain, + cycles, fixed_points, order, permutation_parity, is_even_permutation, + thread_stride_profile, explain, ) ``` @@ -65,7 +66,7 @@ That includes: - `gap_profile` - `bank_conflicts`, `per_group_bank_conflicts` - `coalescing_efficiency`, `segment_analysis`, `per_group_coalescing` -- `cycles`, `fixed_points`, `order` +- `cycles`, `fixed_points`, `order`, `permutation_parity`, `is_even_permutation` Some helpers remain intentionally **affine-only** because they need a real stride tree or linear/F2 structure: @@ -262,17 +263,19 @@ thread_stride_profile(Layout((8, 2), (0, 8))) ## Permutation Analysis -When a layout is bijective (every offset is hit exactly once), it defines -a permutation. Understanding its cycle structure reveals how data moves -through memory --- transpositions, rotations, and fixed points all have -distinct performance implications. +When a layout is injective and its image is a dense interval, it defines +an induced permutation after rebasing that interval to start at 0. +Understanding cycle structure and parity reveals how data moves through +memory --- transpositions, rotations, and fixed points all have distinct +performance implications. ### cycles(layout) Decompose the permutation into disjoint cycles. Fixed points (elements that map to themselves) appear as length-1 cycles. -Raises `ValueError` if the layout is not bijective. +Raises `ValueError` if the layout is not injective or its image is not a +dense interval. ```python # Row-major 3x2: the transpose permutation on a 3x2 matrix. @@ -300,12 +303,38 @@ fixed_points(Layout(4, 1)) # [0, 1, 2, 3] (identity) The permutation order: smallest `k > 0` such that applying the layout `k` times returns to the identity. Equals the LCM of all cycle lengths. -Raises `ValueError` if the layout is not bijective. +Raises `ValueError` if the layout is not injective or its image is not a +dense interval. ```python order(Layout(4, 1)) # 1 (identity) order(Layout((2, 2), (2, 1))) # 2 (single transposition) order(Layout((3, 2), (2, 1))) # 4 (has a 4-cycle) +order(Layout(4, -1)) # 2 (dense reversed interval after rebasing) +``` + +### permutation_parity(layout) + +Return `+1` for even and `-1` for odd induced permutations. + +Parity is computed from the cycle decomposition: each cycle of length `k` +contributes `k-1` transpositions. Empty layouts are treated as identity and +return `+1`. + +Raises `ValueError` under the same conditions as `cycles`/`order`. + +```python +permutation_parity(Layout(8, 1)) # +1 (identity) +permutation_parity(Layout((2, 2), (2, 1))) # -1 (single swap) +permutation_parity(Layout(4, -1)) # +1 ((0 3)(1 2)) +``` + +### is_even_permutation(layout) + +Convenience predicate equivalent to: + +```python +permutation_parity(layout) == 1 ``` ## contiguity(layout)