From fa30bc716cdcd7de1525fde0edbc0c6376cb1964 Mon Sep 17 00:00:00 2001 From: vasco Date: Mon, 17 Jun 2024 23:01:34 +0200 Subject: [PATCH] update --- CHANGELOG.md | 4 +- symmetria/elements/_validators.py | 59 +++++++++++++++++++++++ symmetria/elements/cycle.py | 18 +++---- symmetria/elements/cycle_decomposition.py | 41 +++++----------- symmetria/elements/permutation.py | 32 +++--------- tests/tests_meta/test_order.py | 11 +++-- 6 files changed, 96 insertions(+), 69 deletions(-) create mode 100644 symmetria/elements/_validators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 68efcf5..afe1588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/symmetria/elements/_validators.py b/symmetria/elements/_validators.py new file mode 100644 index 0000000..b7a37b7 --- /dev/null +++ b/symmetria/elements/_validators.py @@ -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) diff --git a/symmetria/elements/cycle.py b/symmetria/elements/cycle.py index cfaef48..fccf749 100644 --- a/symmetria/elements/cycle.py +++ b/symmetria/elements/cycle.py @@ -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"] @@ -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) diff --git a/symmetria/elements/cycle_decomposition.py b/symmetria/elements/cycle_decomposition.py index f547ec5..9efeb23 100644 --- a/symmetria/elements/cycle_decomposition.py +++ b/symmetria/elements/cycle_decomposition.py @@ -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"] @@ -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 diff --git a/symmetria/elements/permutation.py b/symmetria/elements/permutation.py index 0b9a1f2..5d721a5 100644 --- a/symmetria/elements/permutation.py +++ b/symmetria/elements/permutation.py @@ -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"] @@ -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. diff --git a/tests/tests_meta/test_order.py b/tests/tests_meta/test_order.py index 5defa5c..1f582b2 100644 --- a/tests/tests_meta/test_order.py +++ b/tests/tests_meta/test_order.py @@ -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