diff --git a/Makefile b/Makefile index 5d63537..2133c17 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ help: all-test: @echo "[INFO] Run test-suite" - @make tests + @make test @make doctest build: diff --git a/docs/source/pages/API_reference/elements/index.rst b/docs/source/pages/API_reference/elements/index.rst index acc6985..f27f856 100644 --- a/docs/source/pages/API_reference/elements/index.rst +++ b/docs/source/pages/API_reference/elements/index.rst @@ -35,6 +35,11 @@ Here, **P** denotes the class ``Permutation``, **C** the class ``Cycle``, and ** - ✅ - ❌ - ✅ + * - ``__pow__`` + - Power of a permutation + - ✅ + - ✅ + - ✅ * - ``cycle_decomposition`` - Cycle decomposition of the permutation - ✅ diff --git a/symmetria/elements/_interface.py b/symmetria/elements/_interface.py index 38d41fb..8ffaecf 100644 --- a/symmetria/elements/_interface.py +++ b/symmetria/elements/_interface.py @@ -30,6 +30,11 @@ def __mul__(self, other): """Implement multiplication between two object.""" raise NotImplementedError + @abstractmethod + def __pow__(self, power: int) -> "_Element": + """Implement method `__pow__()`.""" + raise NotImplementedError + @abstractmethod def __repr__(self) -> str: """Implement method `__repr__( )`.""" diff --git a/symmetria/elements/cycle.py b/symmetria/elements/cycle.py index 6a25dbe..aa4e89e 100644 --- a/symmetria/elements/cycle.py +++ b/symmetria/elements/cycle.py @@ -265,6 +265,36 @@ def __mul__(self, other: "Cycle") -> "Cycle": "Try to call your cycle on the cycle you would like to compose." ) + def __pow__(self, power: int) -> "Cycle": + """Return the cycle object to the chosen power. + + :param power: the exponent for the power operation. + :type power: int + + :return: the power of the cycle. + :rtype: Cycle + + :example: + >>> from symmetria import Cycle + ... + >>> Cycle(1, 3, 2) ** 0 + Cycle(1, 2, 3) + >>> Cycle(1, 3, 2) ** 1 + Cycle(1, 3, 2) + >>> Cycle(1, 3, 2) ** -1 + Cycle(1, 2, 3) + """ + if isinstance(power, int) is False: + raise TypeError(f"Power operation for type {type(power)} not supported.") + elif self is False or power == 0: + return Cycle(*list(self.domain)) + elif power == 1: + return self + elif power <= -1: + return self.inverse() ** abs(power) + else: + return self(self ** (power - 1)) + def __repr__(self) -> str: r"""Return a string representation of the cycle in the format "Cycle(x, y, z, ...)", where :math:`x, y, z, ... \in \mathbb{N}` are the elements of the cycle. diff --git a/symmetria/elements/cycle_decomposition.py b/symmetria/elements/cycle_decomposition.py index 3781e81..9e4be11 100644 --- a/symmetria/elements/cycle_decomposition.py +++ b/symmetria/elements/cycle_decomposition.py @@ -282,6 +282,36 @@ def __mul__(self, other: "CycleDecomposition") -> "CycleDecomposition": ).cycle_decomposition() raise TypeError(f"Product between types `CycleDecomposition` and {type(other)} is not implemented.") + def __pow__(self, power: int) -> "CycleDecomposition": + """Return the permutation object to the chosen power. + + :param power: the exponent for the power operation. + :type power: int + + :return: the power of the cycle decomposition. + :rtype: Permutation + + :example: + >>> from symmetria import Cycle, CycleDecomposition + ... + >>> CycleDecomposition(Cycle(3), Cycle(1), Cycle(2)) ** 0 + CycleDecomposition(Cycle(1), Cycle(2), Cycle(3)) + >>> CycleDecomposition(Cycle(1, 2), Cycle(3)) ** 1 + CycleDecomposition(Cycle(1, 2), Cycle(3)) + >>> CycleDecomposition(Cycle(1, 2), Cycle(3)) ** -1 + CycleDecomposition(Cycle(1, 2), Cycle(3)) + """ + if isinstance(power, int) is False: + raise TypeError(f"Power operation for type {type(power)} not supported.") + elif self is False or power == 0: + return CycleDecomposition(*[symmetria.elements.cycle.Cycle(i) for i in self.domain]) + elif power == 1: + return self + elif power <= -1: + return self.inverse() ** abs(power) + else: + return self * (self ** (power - 1)) + def __repr__(self) -> str: r"""Return a string representation of the cycle decomposition in the format 'CycleDecomposition(Cycle(x, ...), Cycle(y, ...), ...)', where :math:`x, y, ... \in \mathbb{N}` are diff --git a/symmetria/elements/permutation.py b/symmetria/elements/permutation.py index 1d991fc..5bd4f4c 100644 --- a/symmetria/elements/permutation.py +++ b/symmetria/elements/permutation.py @@ -263,6 +263,36 @@ def __mul__(self, other: "Permutation") -> "Permutation": return Permutation.from_dict(p={idx: self._map[other._map[idx]] for idx in self.domain}) raise TypeError(f"Product between types `Permutation` and {type(other)} is not implemented.") + def __pow__(self, power: int) -> "Permutation": + """Return the permutation object to the chosen power. + + :param power: the exponent for the power operation. + :type power: int + + :return: the power of the permutation. + :rtype: Permutation + + :example: + >>> from symmetria import Permutation + ... + >>> Permutation(3, 1, 2) ** 0 + Permutation(1, 2, 3) + >>> Permutation(3, 1, 2) ** 1 + Permutation(3, 1, 2) + >>> Permutation(3, 1, 2) ** -1 + Permutation(2, 3, 1) + """ + if isinstance(power, int) is False: + raise TypeError(f"Power operation for type {type(power)} not supported.") + elif self is False or power == 0: + return Permutation(*list(self.domain)) + elif power == 1: + return self + elif power <= -1: + return self.inverse() ** abs(power) + else: + return self * (self ** (power - 1)) + def __repr__(self) -> str: r"""Return a string representation of the permutation in the format "Permutation(x, y, z, ...)", where :math:`x, y, z, ... \in \mathbb{N}` are the elements of the permutation. diff --git a/tests/test_factory.py b/tests/test_factory.py index 9d879fa..4011fa0 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -221,6 +221,18 @@ def validate_mul_error(lhs: Any, rhs: Any, error: Type[Exception], msg: str) -> _ = lhs * rhs +def validate_pow(item: Any, power: int, expected_value: Any) -> None: + if item**power != expected_value: + raise ValueError( + f"The expression `{item.rep()} ** {power}` must evaluate {expected_value}, but got {item ** power}." + ) + + +def validate_pow_error(item: Any, power: Any, error: Type[Exception], msg: str) -> None: + with pytest.raises(error, match=msg): + _ = item**power + + def validate_repr(item: Any, expected_value: str) -> None: if item.__repr__() != expected_value: raise ValueError( diff --git a/tests/tests_cycle/test_cases.py b/tests/tests_cycle/test_cases.py index 26ab692..515fb7d 100644 --- a/tests/tests_cycle/test_cases.py +++ b/tests/tests_cycle/test_cases.py @@ -197,6 +197,17 @@ (Cycle(1, 2), 2), (Cycle(1, 3, 2), 3), ] +TEST_POW = [ + (Cycle(1, 3, 2), 0, Cycle(1, 2, 3)), + (Cycle(1, 3, 2), 1, Cycle(1, 3, 2)), + (Cycle(1, 3, 2), -1, Cycle(1, 2, 3)), + (Cycle(1, 3, 2), 2, Cycle(1, 3, 2)(Cycle(1, 3, 2))), + (Cycle(1, 3, 2), -2, Cycle(1, 3, 2).inverse()(Cycle(1, 3, 2).inverse())), +] +TEST_POW_ERROR = [ + (Cycle(1, 2, 3), "asd", TypeError, "Power"), + (Cycle(1, 2, 3), 0.9, TypeError, "Power"), +] TEST_REPR = [ (Cycle(1), "Cycle(1)"), (Cycle(1, 2), "Cycle(1, 2)"), diff --git a/tests/tests_cycle/test_magic_methods.py b/tests/tests_cycle/test_magic_methods.py index 7f080db..4db4f34 100644 --- a/tests/tests_cycle/test_magic_methods.py +++ b/tests/tests_cycle/test_magic_methods.py @@ -4,22 +4,26 @@ validate_eq, validate_int, validate_len, + validate_pow, validate_bool, validate_call, validate_repr, validate_getitem, validate_mul_error, + validate_pow_error, validate_call_error, ) from tests.tests_cycle.test_cases import ( TEST_EQ, TEST_INT, TEST_LEN, + TEST_POW, TEST_BOOL, TEST_CALL, TEST_REPR, TEST_GETITEM, TEST_MUL_ERROR, + TEST_POW_ERROR, TEST_CALL_ERROR, ) @@ -104,6 +108,26 @@ def test_multiplication_error(lhs, rhs, error, msg) -> None: validate_mul_error(lhs=lhs, rhs=rhs, error=error, msg=msg) +@pytest.mark.parametrize( + argnames="cycle, power, expected_value", + argvalues=TEST_POW, + ids=[f"{p}**{q}={r}" for p, q, r in TEST_POW], +) +def test_pow(cycle, power, expected_value) -> None: + """Tests for the method `__pow__()`.""" + validate_pow(item=cycle, power=power, expected_value=expected_value) + + +@pytest.mark.parametrize( + argnames="cycle, power, error, msg", + argvalues=TEST_POW_ERROR, + ids=[f"{p}**{q}" for p, q, _, _ in TEST_POW_ERROR], +) +def test_pow_error(cycle, power, error, msg) -> None: + """Tests for exceptions to the method `__pow__()`.""" + validate_pow_error(item=cycle, power=power, error=error, msg=msg) + + @pytest.mark.parametrize( argnames="cycle, expected_value", argvalues=TEST_REPR, diff --git a/tests/tests_cycle_decomposition/test_cases.py b/tests/tests_cycle_decomposition/test_cases.py index b4b04f5..133c070 100644 --- a/tests/tests_cycle_decomposition/test_cases.py +++ b/tests/tests_cycle_decomposition/test_cases.py @@ -228,6 +228,20 @@ ), (CycleDecomposition(Cycle(1, 2, 3)), "Hello world", TypeError, "Product"), ] +TEST_POW = [ + (CycleDecomposition(Cycle(3), Cycle(1), Cycle(2)), 0, CycleDecomposition(Cycle(3), Cycle(1), Cycle(2))), + (CycleDecomposition(Cycle(1, 2), Cycle(3)), 1, CycleDecomposition(Cycle(1, 2), Cycle(3))), + (CycleDecomposition(Cycle(1, 2), Cycle(3)), -1, CycleDecomposition(Cycle(1, 2), Cycle(3))), + ( + CycleDecomposition(Cycle(1, 3), Cycle(2, 4)), + 2, + CycleDecomposition(Cycle(1, 3), Cycle(2, 4)) * CycleDecomposition(Cycle(1, 3), Cycle(2, 4)), + ), +] +TEST_POW_ERROR = [ + (CycleDecomposition(Cycle(1, 3), Cycle(2, 4)), "abc", TypeError, "Power"), + (CycleDecomposition(Cycle(1, 3), Cycle(2, 4)), 0.9, TypeError, "Power"), +] TEST_REPR = [ (CycleDecomposition(Cycle(1)), "CycleDecomposition(Cycle(1))"), (CycleDecomposition(Cycle(1, 2)), "CycleDecomposition(Cycle(1, 2))"), diff --git a/tests/tests_cycle_decomposition/test_magic_methods.py b/tests/tests_cycle_decomposition/test_magic_methods.py index e8aafb1..f61cbe6 100644 --- a/tests/tests_cycle_decomposition/test_magic_methods.py +++ b/tests/tests_cycle_decomposition/test_magic_methods.py @@ -3,20 +3,24 @@ from symmetria import Cycle, CycleDecomposition from tests.test_factory import ( validate_eq, + validate_pow, validate_bool, validate_call, validate_repr, validate_getitem, validate_mul_error, + validate_pow_error, validate_call_error, ) from tests.tests_cycle_decomposition.test_cases import ( TEST_EQ, + TEST_POW, TEST_BOOL, TEST_CALL, TEST_REPR, TEST_GETITEM, TEST_MUL_ERROR, + TEST_POW_ERROR, TEST_CALL_ERROR, ) @@ -87,6 +91,26 @@ def test_multiplication_error(lhs, rhs, error, msg) -> None: validate_mul_error(lhs=lhs, rhs=rhs, error=error, msg=msg) +@pytest.mark.parametrize( + argnames="cycle_decomposition, power, expected_value", + argvalues=TEST_POW, + ids=[f"{p}**{q}={r}" for p, q, r in TEST_POW], +) +def test_pow(cycle_decomposition, power, expected_value) -> None: + """Tests for the method `__pow__()`.""" + validate_pow(item=cycle_decomposition, power=power, expected_value=expected_value) + + +@pytest.mark.parametrize( + argnames="cycle_decomposition, power, error, msg", + argvalues=TEST_POW_ERROR, + ids=[f"{p}**{q}" for p, q, _, _ in TEST_POW_ERROR], +) +def test_pow_error(cycle_decomposition, power, error, msg) -> None: + """Tests for exceptions to the method `__pow__()`.""" + validate_pow_error(item=cycle_decomposition, power=power, error=error, msg=msg) + + @pytest.mark.parametrize( argnames="cycle_decomposition, expected_value", argvalues=TEST_REPR, diff --git a/tests/tests_permutation/test_cases.py b/tests/tests_permutation/test_cases.py index 7f5b5dc..bca9d39 100644 --- a/tests/tests_permutation/test_cases.py +++ b/tests/tests_permutation/test_cases.py @@ -270,6 +270,17 @@ "Product between", ), ] +TEST_POW = [ + (Permutation(3, 1, 2), 0, Permutation(1, 2, 3)), + (Permutation(3, 1, 2), 1, Permutation(3, 1, 2)), + (Permutation(3, 1, 2), -1, Permutation(2, 3, 1)), + (Permutation(3, 1, 2), 2, Permutation(3, 1, 2) * Permutation(3, 1, 2)), + (Permutation(3, 1, 2), -2, Permutation(3, 1, 2).inverse() * Permutation(3, 1, 2).inverse()), +] +TEST_POW_ERROR = [ + (Permutation(1, 2, 3), "abs", TypeError, "Power"), + (Permutation(1, 2, 3), 9.3, TypeError, "Power"), +] TEST_REPR = [ (Permutation(1), "Permutation(1)"), (Permutation(1, 2), "Permutation(1, 2)"), diff --git a/tests/tests_permutation/test_magic_methods.py b/tests/tests_permutation/test_magic_methods.py index b57f863..b37e6a1 100644 --- a/tests/tests_permutation/test_magic_methods.py +++ b/tests/tests_permutation/test_magic_methods.py @@ -5,11 +5,13 @@ validate_int, validate_len, validate_mul, + validate_pow, validate_str, validate_bool, validate_call, validate_repr, validate_mul_error, + validate_pow_error, validate_call_error, ) from tests.tests_permutation.test_cases import ( @@ -17,11 +19,13 @@ TEST_INT, TEST_LEN, TEST_MUL, + TEST_POW, TEST_STR, TEST_BOOL, TEST_CALL, TEST_REPR, TEST_MUL_ERROR, + TEST_POW_ERROR, TEST_CALL_ERROR, ) @@ -106,6 +110,26 @@ def test_multiplication_error(lhs, rhs, error, msg) -> None: validate_mul_error(lhs=lhs, rhs=rhs, error=error, msg=msg) +@pytest.mark.parametrize( + argnames="permutation, power, expected_value", + argvalues=TEST_POW, + ids=[f"{p}**{q}={r}" for p, q, r in TEST_POW], +) +def test_pow(permutation, power, expected_value) -> None: + """Tests for the method `__pow__()`.""" + validate_pow(item=permutation, power=power, expected_value=expected_value) + + +@pytest.mark.parametrize( + argnames="permutation, power, error, msg", + argvalues=TEST_POW_ERROR, + ids=[f"{p}**{q}" for p, q, _, _ in TEST_POW_ERROR], +) +def test_pow_error(permutation, power, error, msg) -> None: + """Tests for exceptions to the method `__pow__()`.""" + validate_pow_error(item=permutation, power=power, error=error, msg=msg) + + @pytest.mark.parametrize( argnames="permutation, expected_value", argvalues=TEST_REPR,