diff --git a/CHANGELOG.md b/CHANGELOG.md index 7230a23..259e494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ The version is represented by three digits: a.b.c. FEATURE: - `symmetria.Permutation`: add `ascents` method - `symmetria.CyclePermutation`: add `ascents` method +- `symmetria.Permutation`: add `descents` method +- `symmetria.CyclePermutation`: add `descents` method + ## \[0.0.4\] - 2024-05-28 diff --git a/docs/source/pages/API_reference/elements/index.rst b/docs/source/pages/API_reference/elements/index.rst index ee50bd0..c8280a9 100644 --- a/docs/source/pages/API_reference/elements/index.rst +++ b/docs/source/pages/API_reference/elements/index.rst @@ -41,7 +41,7 @@ Here, **P** denotes the class ``Permutation``, **C** the class ``Cycle``, and ** - ✅ - ✅ * - ``ascents`` - - Return the ascents positions of the permutation + - Return the positions of the permutation ascents - ✅ - ❌ - ✅ @@ -60,6 +60,11 @@ Here, **P** denotes the class ``Permutation``, **C** the class ``Cycle``, and ** - ✅ - ✅ - ✅ + * - ``descents`` + - Return the positions of the permutation descents + - ✅ + - ❌ + - ✅ * - ``inverse`` - Compute the inverse of the permutation - ✅ diff --git a/symmetria/elements/cycle_decomposition.py b/symmetria/elements/cycle_decomposition.py index b4cab5d..3c52bc0 100644 --- a/symmetria/elements/cycle_decomposition.py +++ b/symmetria/elements/cycle_decomposition.py @@ -388,7 +388,7 @@ def ascents(self) -> List[int]: [3] """ permutation = symmetria.Permutation.from_cycle_decomposition(self) - return [idx + 1 for idx in range(len(permutation) - 1) if permutation.image[idx] < permutation.image[idx + 1]] + return permutation.ascents() def cycle_decomposition(self) -> "CycleDecomposition": """Return the cycle decomposition of the permutation. @@ -444,6 +444,28 @@ def cycle_type(self) -> Tuple[int]: """ return tuple(sorted(len(cycle) for cycle in self)) + def descents(self) -> List[int]: + r"""Return the descents of the cycle decomposition. + + Recall that a descent of a permutation :math:`\sigma \in S_n`, where :math:`n \in \mathbb{N}`, is any position + :math:`i \sigma(i+1)`. + + :return: The descents of the cycle decomposition. + :rtype: List[int] + + :example: + >>> from symmetria import Cycle, CycleDecomposition + ... + >>> CycleDecomposition(Cycle(1, 2, 3)).descents() + [2] + >>> CycleDecomposition(Cycle(1), Cycle(2), Cycle(3)).descents() + [] + >>> CycleDecomposition(Cycle(2, 3), Cycle(4, 5, 1)).descents() + [1, 2, 4] + """ + permutation = symmetria.Permutation.from_cycle_decomposition(self) + return permutation.descents() + @property def domain(self) -> Iterable[int]: """Return an iterable containing the elements of the domain of the cycle decomposition. diff --git a/symmetria/elements/permutation.py b/symmetria/elements/permutation.py index 02d9d70..20546a4 100644 --- a/symmetria/elements/permutation.py +++ b/symmetria/elements/permutation.py @@ -442,6 +442,27 @@ def cycle_type(self) -> Tuple[int]: """ return tuple(sorted(len(cycle) for cycle in self.cycle_decomposition())) + def descents(self) -> List[int]: + r"""Return the descents of the permutation. + + Recall that a descent of a permutation :math:`\sigma \in S_n`, where :math:`n \in \mathbb{N}`, is any position + :math:`i \sigma(i+1)`. + + :return: The descents of the permutation. + :rtype: List[int] + + :example: + >>> from symmetria import Permutation + ... + >>> Permutation(1, 2, 3).descents() + [] + >>> Permutation(3, 4, 5, 2, 1, 6, 7).descents() + [3, 4] + >>> Permutation(4, 3, 2, 1).descents() + [1, 2, 3] + """ + return [idx + 1 for idx in range(len(self) - 1) if self.image[idx] > self.image[idx + 1]] + @property def domain(self) -> Iterable[int]: """Return an iterable containing the elements of the domain of the permutation. diff --git a/tests/test_factory.py b/tests/test_factory.py index 31beae2..556c10d 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -48,7 +48,7 @@ def validate_from_cycle_decomposition(class_: Any, constructor: Dict, expected_v def validate_ascents(item: Any, expected_value: List[int]) -> None: if item.ascents() != expected_value: raise ValueError( - f"The expression `{item.rep()}.ascents()` must evaluate {expected_value}, " f"but got {item.ascents()}." + f"The expression `{item.rep()}.ascents()` must evaluate {expected_value}, but got {item.ascents()}." ) @@ -75,6 +75,13 @@ def validate_cycle_notation(item: Any, expected_value: str) -> None: ) +def validate_descents(item: Any, expected_value: List[int]) -> None: + if item.descents() != expected_value: + raise ValueError( + f"The expression `{item.rep()}.descents()` must evaluate {expected_value}, but got {item.descents()}." + ) + + def validate_inverse(item: Any, expected_value: Any) -> None: if item.inverse() != expected_value: raise ValueError( diff --git a/tests/tests_cycle_decomposition/test_cases.py b/tests/tests_cycle_decomposition/test_cases.py index 7f381f4..7aec142 100644 --- a/tests/tests_cycle_decomposition/test_cases.py +++ b/tests/tests_cycle_decomposition/test_cases.py @@ -47,6 +47,11 @@ (CycleDecomposition(Cycle(1, 3, 2), Cycle(4)), (1, 3)), (CycleDecomposition(Cycle(1, 2), Cycle(3, 4)), (2, 2)), ] +TEST_DESCENTS = [ + (CycleDecomposition(Cycle(1, 2, 3)), [2]), + (CycleDecomposition(Cycle(1), Cycle(2), Cycle(3)), []), + (CycleDecomposition(Cycle(2, 3), Cycle(4, 5, 1)), [1, 2, 4]), +] TEST_INVERSE = [ (CycleDecomposition(Cycle(1, 2, 3)), CycleDecomposition(Cycle(3, 2, 1))), (CycleDecomposition(Cycle(1, 2), Cycle(3, 4)), CycleDecomposition(Cycle(2, 1), Cycle(4, 3))), diff --git a/tests/tests_cycle_decomposition/test_generic_methods.py b/tests/tests_cycle_decomposition/test_generic_methods.py index b104be4..68c9cea 100644 --- a/tests/tests_cycle_decomposition/test_generic_methods.py +++ b/tests/tests_cycle_decomposition/test_generic_methods.py @@ -10,6 +10,7 @@ validate_inverse, validate_is_even, validate_support, + validate_descents, validate_cycle_type, validate_equivalent, validate_inversions, @@ -29,6 +30,7 @@ TEST_INVERSE, TEST_IS_EVEN, TEST_SUPPORT, + TEST_DESCENTS, TEST_CYCLE_TYPE, TEST_EQUIVALENT, TEST_INVERSIONS, @@ -81,6 +83,16 @@ def test_cycle_type(cycle_decomposition, expected_value) -> None: validate_cycle_type(item=cycle_decomposition, expected_value=expected_value) +@pytest.mark.parametrize( + argnames="cycle_decomposition, expected_value", + argvalues=TEST_DESCENTS, + ids=[f"{p.__repr__()}.descents()={c}" for p, c in TEST_DESCENTS], +) +def test_descents(cycle_decomposition, expected_value) -> None: + """Tests for the method `descents()`.""" + validate_descents(item=cycle_decomposition, expected_value=expected_value) + + @pytest.mark.parametrize( argnames="cycle_decomposition, expected_value", argvalues=TEST_INVERSE, diff --git a/tests/tests_permutation/test_cases.py b/tests/tests_permutation/test_cases.py index 5303b95..48df35f 100644 --- a/tests/tests_permutation/test_cases.py +++ b/tests/tests_permutation/test_cases.py @@ -75,6 +75,11 @@ (Permutation(3, 1, 2), "(1 3 2)"), (Permutation(3, 1, 2, 4, 5, 6), "(1 3 2)(4)(5)(6)"), ] +TEST_DESCENTS = [ + (Permutation(1, 2, 3), []), + (Permutation(3, 4, 5, 2, 1, 6, 7), [3, 4]), + (Permutation(4, 3, 2, 1), [1, 2, 3]), +] TEST_DOMAIN = [ (Permutation(1), range(1, 2)), (Permutation(2, 1), range(1, 3)), diff --git a/tests/tests_permutation/test_generic_methods.py b/tests/tests_permutation/test_generic_methods.py index 568d0e3..39c02b8 100644 --- a/tests/tests_permutation/test_generic_methods.py +++ b/tests/tests_permutation/test_generic_methods.py @@ -11,6 +11,7 @@ validate_inverse, validate_is_even, validate_support, + validate_descents, validate_cycle_type, validate_equivalent, validate_inversions, @@ -32,6 +33,7 @@ TEST_INVERSE, TEST_IS_EVEN, TEST_SUPPORT, + TEST_DESCENTS, TEST_CYCLE_TYPE, TEST_EQUIVALENT, TEST_INVERSIONS, @@ -85,6 +87,16 @@ def test_cycle_notation(permutation, expected_value) -> None: validate_cycle_notation(item=permutation, expected_value=expected_value) +@pytest.mark.parametrize( + argnames="permutation, expected_value", + argvalues=TEST_DESCENTS, + ids=[f"{p.__repr__()}.descents()={c}" for p, c in TEST_DESCENTS], +) +def test_descents(permutation, expected_value) -> None: + """Tests for the method `descents()`.""" + validate_descents(item=permutation, expected_value=expected_value) + + @pytest.mark.parametrize( argnames="permutation, expected_value", argvalues=TEST_DOMAIN,