Skip to content
Merged
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
45 changes: 37 additions & 8 deletions docs/analysis_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
```

Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions src/tensor_layouts/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"cycles",
"fixed_points",
"order",
"permutation_parity",
"is_even_permutation",
"contiguity",
"mode_contiguity",
"slice_contiguity",
Expand Down Expand Up @@ -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
# =============================================================================
Expand Down
70 changes: 70 additions & 0 deletions tests/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading