Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add sgn method #37

Merged
merged 4 commits into from
May 19, 2024
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ The version is represented by three digits: a.b.c.

## Unreleased


FEATURE:

- `symmetria.Permutation`: add `sgn` method
- `symmetria.Cycle`: add `sgn` method
- `symmetria.CyclePermutation`: add `sgn` method

MAINTENANCE:

- `tests.tests_meta.test_order.py`: add test suite for order of methods


## \[0.0.1\] - 2024-05-16

MAINTENANCE:
Expand Down
15 changes: 10 additions & 5 deletions docs/source/pages/API_reference/elements/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,6 @@ Here, **P** denotes the class ``Permutation``, **C** the class ``Cycle``, and **
- ✅
- ❌
- ❌
* - ``support``
- Return the support of the permutation
- ✅
- ✅
- ✅
* - ``orbit``
- Compute image of a given element under the permutation
- ✅
Expand All @@ -75,6 +70,16 @@ Here, **P** denotes the class ``Permutation``, **C** the class ``Cycle``, and **
- ✅
- ✅
- ✅
* - ``sgn``
- Return the sign of the permutation
- ✅
- ✅
- ✅
* - ``support``
- Return the support of the permutation
- ✅
- ✅
- ✅



Expand Down
23 changes: 23 additions & 0 deletions symmetria/elements/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,29 @@ def order(self) -> int:
"""
return len(self)

def sgn(self) -> int:
r"""Return the sign of the cycle.

Recall that the sign, signature, or signum of a permutation :math:`\sigma` is defined as +1 if :math:`\sigma`
is even, and -1 if :math:`\sigma` is odd.

To compute the sign of a cycle, we use the fact that a cycle is odd if and only if it has even length.

:return: 1 if the cycle is even, -1 if the cycle is odd.
:rtype: int

:example:
>>> Cycle(1).sgn()
1
>>> Cycle(1, 2).sgn()
-1
>>> Cycle(1, 2, 3, 4, 5, 6).sgn()
-1
>>> Cycle(1, 2, 3, 4, 5, 6, 7).sgn()
1
"""
return -1 if len(self) % 2 == 0 else 1

def support(self) -> Set[int]:
"""Return a set containing the indices in the domain of the cycle whose images are different from their
respective indices, i.e., the set of :math:`n` in the cycle domain which are not mapped to itself.
Expand Down
24 changes: 23 additions & 1 deletion symmetria/elements/cycle_decomposition.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from math import lcm
from math import lcm, prod
from typing import Any, Set, Dict, List, Tuple, Union, Iterable
from itertools import combinations

Expand Down Expand Up @@ -490,6 +490,28 @@ def order(self) -> int:
"""
return lcm(*[len(cycle) for cycle in self])

def sgn(self) -> int:
r"""Return the sign of the cycle decomposition.

Recall that the sign, signature, or signum of a permutation :math:`\sigma` is defined as +1 if :math:`\sigma`
is even, and -1 if :math:`\sigma` is odd.

To compute the sign of a cycle decomposition, we use the fact that the sign is a homomorphism of groups, i.e.,
the sign of the cycle decomposition is just the product of the signs of the cycle componing it.

:return: 1 if the cycle decomposition is even, -1 if the cycle decomposition is odd.
:rtype: int

:example:
>>> CycleDecomposition(Cycle(1)).sgn()
1
>>> CycleDecomposition(Cycle(1, 2), Cycle(3)).sgn()
-1
>>> CycleDecomposition(Cycle(1), Cycle(2, 4, 7, 6), Cycle(3, 5)).sgn()
1
"""
return prod([cycle.sgn() for cycle in self])

def support(self) -> Set[int]:
"""Return a set containing the indices in the domain of the permutation whose images are different from
their respective indices, i.e., the set of :math:`n` in the permutation domain which are not mapped to itself.
Expand Down
21 changes: 20 additions & 1 deletion symmetria/elements/permutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,27 @@ def order(self) -> int:
"""
return self.cycle_decomposition().order()

def sgn(self) -> int:
r"""Return the sign of the permutation.

Recall that the sign, signature, or signum of a permutation :math:`\sigma` is defined as +1 if :math:`\sigma`
is even, and -1 if :math:`\sigma` is odd.

:return: 1 if the permutation is even, -1 if the permutation is odd.
:rtype: int

:example:
>>> Permutation(1).sgn()
1
>>> Permutation(2, 1).sgn()
-1
>>> Permutation(2, 3, 4, 5, 6, 1).sgn()
-1
"""
return self.cycle_decomposition().sgn()

def support(self) -> Set[int]:
"""Return a set containing the indices in the domain of the permutation whose images are different from their
r"""Return a set containing the indices in the domain of the permutation whose images are different from their
respective indices, i.e., the set of :math:`n` in the permutation domain which are not mapped to itself.

:return: The support set of the permutation.
Expand Down
13 changes: 9 additions & 4 deletions tests/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ def validate_equivalent(lhs: Any, rhs: Any, expected_value: bool) -> None:
def validate_domain(item: Any, expected_value: bool) -> None:
if item.domain != expected_value:
raise ValueError(
f"The expression `{item.rep()}.domain()` must evaluate {expected_value}, " f"but got {item.domain}."
f"The expression `{item.rep()}.domain()` must evaluate {expected_value}, but got {item.domain}."
)


def validate_map(item: Any, expected_value: Dict[int, int]) -> None:
if item.map != expected_value:
raise ValueError(f"The expression `{item.rep()}.map()` must evaluate {expected_value}, " f"but got {item.map}.")
raise ValueError(f"The expression `{item.rep()}.map()` must evaluate {expected_value}, but got {item.map}.")


def validate_orbit(element: Any, item: Any, expected_value: int) -> None:
Expand All @@ -93,14 +93,19 @@ def validate_orbit(element: Any, item: Any, expected_value: int) -> None:
def validate_order(item: Any, expected_value: int) -> None:
if item.order() != expected_value:
raise ValueError(
f"The expression `{item.rep()}.order()` must evaluate {expected_value}, " f"but got {item.order()}."
f"The expression `{item.rep()}.order()` must evaluate {expected_value}, but got {item.order()}."
)


def validate_sgn(item: Any, expected_value: int) -> None:
if item.sgn() != expected_value:
raise ValueError(f"The expression `{item.rep()}.sgn()` must evaluate {expected_value}, but got {item.sgn()}.")


def validate_support(item: Any, expected_value: Set[int]) -> None:
if item.support() != expected_value:
raise ValueError(
f"The expression `{item.rep()}.support()` must evaluate {expected_value}, " f"but got {item.support()}."
f"The expression `{item.rep()}.support()` must evaluate {expected_value}, but got {item.support()}."
)


Expand Down
20 changes: 14 additions & 6 deletions tests/tests_cycle/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
(Cycle(1, 2), True),
(Cycle(1, 2, 3), True),
]
TEST_MAP = [
(Cycle(1), {1: 1}),
(Cycle(13), {13: 13}),
(Cycle(1, 2), {1: 2, 2: 1}),
(Cycle(1, 2, 3), {1: 2, 2: 3, 3: 1}),
]
TEST_ORBIT = [
(Cycle(3, 1, 2), 1, [1, 2, 3]),
(Cycle(3, 1, 2), "abc", ["abc", "cab", "bca"]),
Expand All @@ -78,18 +84,20 @@
(Cycle(1, 2), 2),
(Cycle(1, 2, 3), 3),
]
TEST_SGN = [
(Cycle(1), 1),
(Cycle(13), 1),
(Cycle(1, 2), -1),
(Cycle(1, 2, 3), 1),
(Cycle(1, 2, 3, 4, 5, 6), -1),
(Cycle(1, 2, 3, 4, 5, 6, 7), 1),
]
TEST_SUPPORT = [
(Cycle(1), set()),
(Cycle(13), set()),
(Cycle(1, 2), {1, 2}),
(Cycle(1, 2, 3), {1, 2, 3}),
]
TEST_MAP = [
(Cycle(1), {1: 1}),
(Cycle(13), {13: 13}),
(Cycle(1, 2), {1: 2, 2: 1}),
(Cycle(1, 2, 3), {1: 2, 2: 3, 3: 1}),
]

############################
# TEST CASES MAGIC METHODS #
Expand Down
12 changes: 12 additions & 0 deletions tests/tests_cycle/test_generic_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from tests.test_factory import (
validate_map,
validate_sgn,
validate_orbit,
validate_order,
validate_domain,
Expand All @@ -12,6 +13,7 @@
)
from tests.tests_cycle.test_cases import (
TEST_MAP,
TEST_SGN,
TEST_ORBIT,
TEST_ORDER,
TEST_DOMAIN,
Expand Down Expand Up @@ -107,6 +109,16 @@ def test_order(cycle, expected_value) -> None:
validate_order(item=cycle, expected_value=expected_value)


@pytest.mark.parametrize(
argnames="cycle, expected_value",
argvalues=TEST_SGN,
ids=[f"{p}.sgn()={o}" for p, o in TEST_SGN],
)
def test_sgn(cycle, expected_value) -> None:
"""Tests for the method `sgn()`."""
validate_sgn(item=cycle, expected_value=expected_value)


@pytest.mark.parametrize(
argnames="cycle, expected_value",
argvalues=TEST_SUPPORT,
Expand Down
5 changes: 5 additions & 0 deletions tests/tests_cycle_decomposition/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
(CycleDecomposition(Cycle(1), Cycle(2, 4, 7, 6), Cycle(3, 5)), {2, 4, 7, 6, 3, 5}),
(CycleDecomposition(Cycle(1, 6, 2, 4, 7), Cycle(3, 5)), {1, 6, 2, 4, 7, 3, 5}),
]
TEST_SGN = [
(CycleDecomposition(Cycle(1)), 1),
(CycleDecomposition(Cycle(1, 2), Cycle(3)), -1),
(CycleDecomposition(Cycle(1), Cycle(2, 4, 7, 6), Cycle(3, 5)), 1),
]

############################
# TEST CASES MAGIC METHODS #
Expand Down
12 changes: 12 additions & 0 deletions tests/tests_cycle_decomposition/test_generic_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from tests.test_factory import (
validate_map,
validate_sgn,
validate_orbit,
validate_order,
validate_support,
Expand All @@ -12,6 +13,7 @@
)
from tests.tests_cycle_decomposition.test_cases import (
TEST_MAP,
TEST_SGN,
TEST_ORBIT,
TEST_ORDER,
TEST_SUPPORT,
Expand Down Expand Up @@ -92,6 +94,16 @@ def test_order(cycle_decomposition, expected_value) -> None:
validate_order(item=cycle_decomposition, expected_value=expected_value)


@pytest.mark.parametrize(
argnames="cycle_decomposition, expected_value",
argvalues=TEST_SGN,
ids=[f"{p}.sgn()={o}" for p, o in TEST_SGN],
)
def test_sgn(cycle_decomposition, expected_value) -> None:
"""Tests for the method `sgn()`."""
validate_sgn(item=cycle_decomposition, expected_value=expected_value)


@pytest.mark.parametrize(
argnames="cycle_decomposition, expected_value",
argvalues=TEST_SUPPORT,
Expand Down
1 change: 1 addition & 0 deletions tests/tests_permutation/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
(Permutation(1, 4, 3, 2), 2),
(Permutation(1, 4, 5, 7, 3, 2, 6), 4),
]
TEST_SGN = [(Permutation(1), 1), (Permutation(2, 1), -1), (Permutation(2, 3, 4, 5, 6, 1), -1)]

############################
# TEST CASES MAGIC METHODS #
Expand Down
12 changes: 12 additions & 0 deletions tests/tests_permutation/test_generic_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from tests.test_factory import (
validate_map,
validate_sgn,
validate_orbit,
validate_order,
validate_domain,
Expand All @@ -13,6 +14,7 @@
)
from tests.tests_permutation.test_cases import (
TEST_MAP,
TEST_SGN,
TEST_ORBIT,
TEST_ORDER,
TEST_DOMAIN,
Expand Down Expand Up @@ -119,6 +121,16 @@ def test_one_line_notation(permutation, expected_value) -> None:
)


@pytest.mark.parametrize(
argnames="permutation, expected_value",
argvalues=TEST_SGN,
ids=[f"{p.rep()}.sgn()={s}" for p, s in TEST_SGN],
)
def test_sgn(permutation, expected_value) -> None:
"""Tests for the method `sgn()`."""
validate_sgn(item=permutation, expected_value=expected_value)


@pytest.mark.parametrize(
argnames="permutation, expected_value",
argvalues=TEST_ORDER,
Expand Down
Loading