Skip to content

Commit

Permalink
[FEATURE] Add __pow__ method (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
VascoSch92 authored May 28, 2024
1 parent 6e58e8a commit a288755
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ help:

all-test:
@echo "[INFO] Run test-suite"
@make tests
@make test
@make doctest

build:
Expand Down
5 changes: 5 additions & 0 deletions docs/source/pages/API_reference/elements/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
- ✅
Expand Down
5 changes: 5 additions & 0 deletions symmetria/elements/_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__( )`."""
Expand Down
30 changes: 30 additions & 0 deletions symmetria/elements/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions symmetria/elements/cycle_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions symmetria/elements/permutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions tests/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions tests/tests_cycle/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"),
Expand Down
24 changes: 24 additions & 0 deletions tests/tests_cycle/test_magic_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions tests/tests_cycle_decomposition/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))"),
Expand Down
24 changes: 24 additions & 0 deletions tests/tests_cycle_decomposition/test_magic_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions tests/tests_permutation/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"),
Expand Down
24 changes: 24 additions & 0 deletions tests/tests_permutation/test_magic_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@
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 (
TEST_EQ,
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,
)

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit a288755

Please sign in to comment.