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 __pow__ method #65

Merged
merged 1 commit into from
May 28, 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
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
Loading