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

[ENHANCEMENT] Add __new__ to Cycle, CycleDecomposition and Permutation #107

Merged
merged 1 commit into from
Jun 17, 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ FEATURE:

ENHANCEMENT:
- `symmetria.Permutation`: change how the sign is computed

- `symmetria.Permutation`: add `__new__` method to the class
- `symmetria.CyclePermutation`: add `__new__` method to the class
- `symmetria.Cycle`: add `__new__` method to the class

## \[0.2.0\] - 2024-06-12

Expand Down
59 changes: 59 additions & 0 deletions symmetria/elements/_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Tuple
from itertools import combinations


def _validate_cycle(cycle: Tuple[int, ...]) -> None:
"""Private method to validate and standardize a set of integers to form a cycle.

A tuple is eligible to be a cycle if it contains only strictly positive integers.
"""
for element in cycle:
if isinstance(element, int) is False:
raise ValueError(f"Expected `int` type, but got {type(element)}.")
if element < 1:
raise ValueError(f"Expected all strictly positive values, but got {element}.")


def _validate_cycle_decomposition(cycles: Tuple["Cycle", ...]) -> None:
"""Private method to validate and standardize a tuple of cycles to become a cycle decomposition.

A tuple of cycles is eligible to be a cycle decomposition if and only if:
- every pair of cycles is disjoint, meaning their supports are disjoint;
- every element from 1 to the largest permuted element is included in at least one cycle.
"""
# checks that the cycles are disjoint
for cycle_a, cycle_b in combinations(cycles, 2):
if set(cycle_a.elements) & set(cycle_b.elements):
raise ValueError(f"The cycles {cycle_a} and {cycle_b} don't have disjoint support.")

# checks that every element is included in a cycle
elements = {element for cycle in cycles for element in cycle.elements}
if set(range(1, len(elements) + 1)) != elements:
raise ValueError(
"Every element from 1 to the biggest permuted element must be included in some cycle,\n "
f"but this is not the case for the element(s): {set(range(1, len(elements) + 1)).difference(elements)}"
)


def _validate_permutation(image: Tuple[int, ...]) -> None:
"""Private method to check if a set of integers is eligible as image for a permutation.

Recall that, a tuple of integers represent the image of a permutation if the following conditions hold:
- all the integers are strictly positive;
- all the integers are bounded by the total number of integers;
- there are no integer repeated.
"""
values = set()
for img in image:
if isinstance(img, int) is False:
raise ValueError(f"Expected `int` type, but got {type(img)}.")
elif img < 1:
raise ValueError(f"Expected all strictly positive values, but got {img}")
elif img > len(image):
raise ValueError(f"The permutation is not injecting on its image. Indeed, {img} is not in the image.")
elif img in values:
raise ValueError(
f"It seems that the permutation is not bijective. Indeed, {img} has two, or more, pre-images."
)
else:
values.add(img)
18 changes: 8 additions & 10 deletions symmetria/elements/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import symmetria.elements.cycle_decomposition
from symmetria.elements._base import _Element
from symmetria.elements._utils import _pretty_print_table
from symmetria.elements._validators import _validate_cycle

__all__ = ["Cycle"]

Expand Down Expand Up @@ -42,23 +43,20 @@ class Cycle(_Element):

__slots__ = ["_cycle", "_domain"]

def __new__(cls, *cycle: int) -> "Cycle":
_validate_cycle(cycle=cycle)
return super().__new__(cls)

def __init__(self, *cycle: int) -> None:
self._cycle: Tuple[int, ...] = self._validate_and_standardize(cycle)
self._cycle: Tuple[int, ...] = self._standardization(cycle=cycle)
self._domain: Iterable[int] = range(1, max(self._cycle) + 1)

@staticmethod
def _validate_and_standardize(cycle: Tuple[int, ...]) -> Tuple[int, ...]:
"""Private method to validate and standardize a set of integers to form a cycle.
def _standardization(cycle: Tuple[int, ...]) -> Tuple[int, ...]:
"""Private method to standardize a set of integers to form a cycle.

A tuple is eligible to be a cycle if it contains only strictly positive integers.
The standard form for a cycle is the (unique) one where the first element is the smallest.
"""
for element in cycle:
if isinstance(element, int) is False:
raise ValueError(f"Expected `int` type, but got {type(element)}.")
if element < 1:
raise ValueError(f"Expected all strictly positive values, but got {element}.")

smallest_element_index = cycle.index(min(cycle))
if smallest_element_index == 0:
return tuple(cycle)
Expand Down
41 changes: 12 additions & 29 deletions symmetria/elements/cycle_decomposition.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from math import lcm, prod
from typing import Any, Set, Dict, List, Tuple, Union, Iterable
from itertools import combinations
from collections import OrderedDict

import symmetria.elements.cycle
import symmetria.elements.permutation
from symmetria.elements._base import _Element
from symmetria.elements._utils import _pretty_print_table
from symmetria.elements._validators import _validate_cycle_decomposition

__all__ = ["CycleDecomposition"]

Expand Down Expand Up @@ -45,41 +45,24 @@ class CycleDecomposition(_Element):

__slots__ = ["_cycles", "_domain"]

def __new__(cls, *cycles: "Cycle") -> "CycleDecomposition":
_validate_cycle_decomposition(cycles=cycles)
return super().__new__(cls)

def __init__(self, *cycles: "Cycle") -> None:
self._cycles: Tuple["Cycle", ...] = self._validate_and_standardize(
cycles=cycles,
)
self._cycles: Tuple["Cycle", ...] = self._standardization(cycles=cycles)
self._domain: Iterable[int] = range(
1,
max(max(cycle.elements) for cycle in self._cycles) + 1,
)

@staticmethod
def _validate_and_standardize(cycles: Tuple["Cycle", ...]) -> Tuple["Cycle", ...]:
"""Private method to validate and standardize a tuple of cycles to become a cycle decomposition.

A tuple of cycles is eligible to be a cycle decomposition if and only if:
- every pair of cycles is disjoint, meaning their supports are disjoint;
- every element from 1 to the largest permuted element is included in at least one cycle.
Furthermore, the cycle decomposition is standardized, meaning the cycles are ordered by the first
element of each cycle in increasing order.
"""
# checks that the cycles are disjoint
for cycle_a, cycle_b in combinations(cycles, 2):
if set(cycle_a.elements) & set(cycle_b.elements):
raise ValueError(f"The cycles {cycle_a} and {cycle_b} don't have disjoint support.")

# checks that every element is included in a cycle
elements = {element for cycle in cycles for element in cycle.elements}
if set(range(1, len(elements) + 1)) != elements:
raise ValueError(
"Every element from 1 to the biggest permuted element must be included in some cycle,\n "
f"but this is not the case for the element(s): {set(range(1, len(elements) + 1)).difference(elements)}"
)

# standardization
cycles = sorted(cycles, key=lambda cycle: cycle[0])
return tuple(cycles)
def _standardization(cycles: Tuple["Cycle", ...]) -> Tuple["Cycle", ...]:
"""Private method to standardize a tuple of cycles to become a cycle decomposition.

A cycle decomposition is standardized if the cycles are ordered by increasingly the first element of each cycle.
"""
return tuple(sorted(cycles, key=lambda cycle: cycle[0]))

def __bool__(self) -> bool:
r"""Check if the cycle decomposition is non-empty, i.e., it is different from the identity
Expand Down
32 changes: 6 additions & 26 deletions symmetria/elements/permutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import symmetria.elements.cycle_decomposition
from symmetria.elements._base import _Element
from symmetria.elements._utils import _pretty_print_table
from symmetria.elements._validators import _validate_permutation

__all__ = ["Permutation"]

Expand Down Expand Up @@ -35,36 +36,15 @@ class Permutation(_Element):

__slots__ = ["_map", "_domain", "_image"]

def __new__(cls, *image: int) -> "Permutation":
_validate_permutation(image=image)
return super().__new__(cls)

def __init__(self, *image: int) -> None:
self._map: Dict[int, int] = self._validate_image(image)
self._map: Dict[int, int] = dict(enumerate(image, 1))
self._domain: Iterable[int] = range(1, len(self._map) + 1)
self._image: Tuple[int] = tuple(image)

@staticmethod
def _validate_image(image: Tuple[int, ...]) -> Dict[int, int]:
"""Private method to check if a set of integers is eligible as image for a permutation.

Recall that, a tuple of integers represent the image of a permutation if the following conditions hold:
- all the integers are strictly positive;
- all the integers are bounded by the total number of integers;
- there are no integer repeated.
"""
_map = {}
for idx, img in enumerate(image):
if isinstance(img, int) is False:
raise ValueError(f"Expected `int` type, but got {type(img)}.")
if img < 1:
raise ValueError(f"Expected all strictly positive values, but got {img}")
elif img > len(image):
raise ValueError(f"The permutation is not injecting on its image. Indeed, {img} is not in the image.")
elif img in _map.values():
raise ValueError(
f"It seems that the permutation is not bijective. Indeed, {img} has two, or more, pre-images."
)
else:
_map[idx + 1] = img
return _map

def __bool__(self) -> bool:
"""Check if the permutation is different from the identity permutation.

Expand Down
11 changes: 8 additions & 3 deletions tests/tests_meta/test_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@ def script_methods(self) -> List:
return [item.name for item in leaf.body if isinstance(item, ast.FunctionDef)]

@staticmethod
def test_init_first_method(script_methods) -> None:
def test_new_first_method(script_methods) -> None:
if "__new__" in script_methods:
assert script_methods.index("__new__") == 0, "The `__new__` method is not in the first position."

@staticmethod
def test_init_second_method(script_methods) -> None:
if "__init__" in script_methods:
assert script_methods.index("__init__") == 0, "The `__init__` method is not in first position."
assert script_methods.index("__init__") == 1, "The `__init__` method is not in the second position."

@staticmethod
def test_order(script_methods) -> None:
script_methods = [
method
for method in script_methods
if method != "__init__" # exclude __init__
if method not in {"__new__", "__init__"} # exclude __new__ and __init__
and not (method.startswith("_") and not method.startswith("__")) # exclude private method but not magic
]
assert sorted(script_methods) == script_methods
Expand Down
Loading