diff --git a/CHANGELOG.md b/CHANGELOG.md index afe1588..da0afcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ FEATURE: - `symmetria.Permutation`: add `degree` method - `symmetria.CyclePermutation`: add `degree` method - `symmetria.Cycle`: add `degree` method +- `symmetria.random`: add random module to create random permutations ENHANCEMENT: - `symmetria.Permutation`: change how the sign is computed diff --git a/docs/source/index.rst b/docs/source/index.rst index 1b71a82..e19c9c2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -84,6 +84,38 @@ with the symmetric group and its elements. API reference + .. grid-item-card:: :octicon:`iterations` Generators + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Generating permutations + + .. button-ref:: pages/API_reference/generators/index + :ref-type: doc + :color: primary + :outline: + :click-parent: + :expand: + + API Reference + + .. grid-item-card:: :octicon:`file-code` CLI + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Command Line Interface + + .. button-ref:: pages/cli/command_line_interface + :ref-type: doc + :color: primary + :outline: + :click-parent: + :expand: + + API reference + .. warning:: The documentations is still a working in progress. diff --git a/docs/source/pages/API_reference/generators/deterministic.rst b/docs/source/pages/API_reference/generators/deterministic.rst new file mode 100644 index 0000000..f49a994 --- /dev/null +++ b/docs/source/pages/API_reference/generators/deterministic.rst @@ -0,0 +1,44 @@ +Deterministic Generation +======================== + +`Symmetria` provides a way to generate all the permutations of a given degree. The generation follows different +algorithms which can be specified. + +You can use the generator of permutations in the following way + +.. code-block:: python + + import symmetria + + permutations = symmetria.generate(algorithm="lexicographic", degree=3) + +A list of implemented algorithms to generate permutations: + +.. list-table:: overview + :widths: 35 50 15 + :header-rows: 1 + + * - Algorithm + - Description + - Reference + * - ``lexicographic`` + - The permutations are generate following the **lexicographic order**. + - `[1]`_ + * - ``heap`` + - The permutations are generate following the **Heap's algorithm**. + - `[2]`_ + * - ``steinhaus-johnson-trotter`` + - The permutations are generate following the **Steinhaus-Johnson-Trotter algorithm**. + - `[3]`_ + +.. _[1]: https://en.wikipedia.org/wiki/Lexicographic_order +.. _[2]: https://academic.oup.com/comjnl/article/6/3/293/360213 +.. _[3]: https://en.wikipedia.org/wiki/Steinhaus–Johnson–Trotter_algorithm + +==== + +The API of the method is given as following: + +.. automodule:: symmetria.generators.algorithm.api + :members: generate + :exclude-members: random, random_generator diff --git a/docs/source/pages/API_reference/generators/generate.rst b/docs/source/pages/API_reference/generators/generate.rst deleted file mode 100644 index 5378b4f..0000000 --- a/docs/source/pages/API_reference/generators/generate.rst +++ /dev/null @@ -1,18 +0,0 @@ -Generate -======== - -To use the generator of permutations, you can or - -.. code-block:: python - - import symmetria - - permutations = symmetria.generate(algorithm="lexicographic", degree=3) - - - -The API of the method is given as following: - -.. automodule:: symmetria - :members: generate - :exclude-members: Cycle, CycleDecomposition, Permutation \ No newline at end of file diff --git a/docs/source/pages/API_reference/generators/index.rst b/docs/source/pages/API_reference/generators/index.rst index 5d2a970..96dda09 100644 --- a/docs/source/pages/API_reference/generators/index.rst +++ b/docs/source/pages/API_reference/generators/index.rst @@ -1,36 +1,15 @@ Generators ========== -`Symmetria` provides a way to generate all the permutations of a given degree. The generation follows different -algorithms which can be specified. +`Symmetria` provides a way to generate permutations of a given degree in two way: -.. note:: The permutation are generated following a well-defined pattern, i.e., they are not random. +- algorithmically, i.e., the permutations are generated following a given algorithm; +- randomly, i.e., the permutations are generate randomly. -A list of implemented algorithms to generate permutations: - -.. list-table:: overview - :widths: 35 50 15 - :header-rows: 1 - - * - Algorithm - - Description - - Reference - * - ``lexicographic`` - - The permutations are generate following the **lexicographic order**. - - `[1]`_ - * - ``heap`` - - The permutations are generate following the **Heap's algorithm**. - - `[2]`_ - * - ``steinhaus-johnson-trotter`` - - The permutations are generate following the **Steinhaus-Johnson-Trotter algorithm**. - - `[3]`_ - -.. _[1]: https://en.wikipedia.org/wiki/Lexicographic_order -.. _[2]: https://academic.oup.com/comjnl/article/6/3/293/360213 -.. _[3]: https://en.wikipedia.org/wiki/Steinhaus–Johnson–Trotter_algorithm .. toctree:: :maxdepth: 1 :hidden: - generate + deterministic + random diff --git a/docs/source/pages/API_reference/generators/random.rst b/docs/source/pages/API_reference/generators/random.rst new file mode 100644 index 0000000..f5f3758 --- /dev/null +++ b/docs/source/pages/API_reference/generators/random.rst @@ -0,0 +1,48 @@ +Random Generation +================= + +`Symmetria` provides a way to generate random permutation. + +You can use the random generation of permutations in the following way + +.. code-block:: python + + import symmetria + + permutations = symmetria.random_generator(algorithm="lexicographic", degree=3) + +If you don't want to have a generator and you want to just have a singular random permutation +you can use write + +.. code-block:: python + + import symmetria + + permutation = symmetria.random(algorithm="lexicographic", degree=3) + + +A list of implemented algorithms to generate permutations: + +.. list-table:: overview + :widths: 35 50 15 + :header-rows: 1 + + * - Algorithm + - Description + - Reference + * - ``random`` + - A permutation is generated by choosing the integer uniformly. + - + * - ``fisher-yates`` + - The permutations are generate following the **Steinhaus-Johnson-Trotter algorithm**. + - `[1]`_ + +.. _[1]: https://en.wikipedia.org/wiki/Lexicographic_order + +==== + +The API of the method is given as following: + +.. automodule:: symmetria.generators.random.api + :members: random_generator + :exclude-members: random \ No newline at end of file diff --git a/symmetria/__init__.py b/symmetria/__init__.py index 530e0c4..d093d26 100644 --- a/symmetria/__init__.py +++ b/symmetria/__init__.py @@ -1,7 +1,16 @@ from symmetria.elements.cycle import Cycle -from symmetria.generators.api import generate from symmetria.elements.permutation import Permutation +from symmetria.generators.random.api import random, random_generator +from symmetria.generators.algorithm.api import generate from symmetria.elements.cycle_decomposition import CycleDecomposition __version__ = "0.2.0" -__all__ = ["__version__", "generate", "Permutation", "Cycle", "CycleDecomposition"] +__all__ = [ + "__version__", + "generate", + "random", + "random_generator", + "Permutation", + "Cycle", + "CycleDecomposition", +] diff --git a/symmetria/generators/_validators.py b/symmetria/generators/_validators.py new file mode 100644 index 0000000..b0abf64 --- /dev/null +++ b/symmetria/generators/_validators.py @@ -0,0 +1,26 @@ +from typing import List + + +def _check_algorithm_parameter(value: str, supported: List[str]) -> None: + """Private method to check the value provided for the parameter `algorithm`. + + Recall that the parameter `algorithm` must be a string present in the list `supported`. + """ + if isinstance(value, str) is False: + raise TypeError(f"The parameter `algorithm` must be of type string, but {type(value)} was provided.") + if value not in supported: + raise ValueError( + f"The given algorithm ({value}) is not supported. \n " + f"Here, a list of supported algorithm for generations of permutations {supported}." + ) + + +def _check_degree_parameter(value: int) -> None: + """Private method to check the value provided for the parameter `degree`. + + Recall that the parameter `degree` must be a non-negative integer different from zero. + """ + if isinstance(value, int) is False: + raise TypeError(f"The parameter `degree` must be of type int, but {type(value)} was provided.") + if value < 1: + raise ValueError(f"The parameter `degree` must be a non-zero positive integer, but {value} was provided.") diff --git a/symmetria/generators/algorithm/__init__.py b/symmetria/generators/algorithm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/symmetria/generators/_algorithms.py b/symmetria/generators/algorithm/_algorithms.py similarity index 94% rename from symmetria/generators/_algorithms.py rename to symmetria/generators/algorithm/_algorithms.py index d19efce..8790bf0 100644 --- a/symmetria/generators/_algorithms.py +++ b/symmetria/generators/algorithm/_algorithms.py @@ -1,9 +1,10 @@ from typing import List, Generator import symmetria.elements.permutation +from symmetria import Permutation -def _lexicographic(degree: int, start: List[int]) -> Generator["Permutation", None, None]: +def _lexicographic(degree: int, start: List[int]) -> Generator[Permutation, None, None]: """Private method to generate all the permutations of degree `degree` in lexicographic order. The algorithm is described as follows: @@ -38,7 +39,7 @@ def _lexicographic(degree: int, start: List[int]) -> Generator["Permutation", No permutation[k + 1 :] = reversed(permutation[k + 1 :]) -def _heap(degree: int, start: List[int]) -> Generator["Permutation", None, None]: +def _heap(degree: int, start: List[int]) -> Generator[Permutation, None, None]: """Private method to generate all the permutations of degree `degree` using the Heap's algorithm. A description of the algorithm can be founded in the article: @@ -63,7 +64,7 @@ def _heap(degree: int, start: List[int]) -> Generator["Permutation", None, None] yield from _heap(k - 1, permutation) -def _steinhaus_johnson_trotter(degree: int, start: List[int]) -> Generator["Permutation", None, None]: +def _steinhaus_johnson_trotter(degree: int, start: List[int]) -> Generator[Permutation, None, None]: """Private method to generate all the permutations of degree `degree` using the Steinhaus-Johnson-Trotter algorithm. A description of the algorithm is given at: diff --git a/symmetria/generators/api.py b/symmetria/generators/algorithm/api.py similarity index 60% rename from symmetria/generators/api.py rename to symmetria/generators/algorithm/api.py index d3f0838..5c018d4 100644 --- a/symmetria/generators/api.py +++ b/symmetria/generators/algorithm/api.py @@ -1,6 +1,8 @@ from typing import List, Generator -from symmetria.generators._algorithms import ( +from symmetria import Permutation +from symmetria.generators._validators import _check_degree_parameter, _check_algorithm_parameter +from symmetria.generators.algorithm._algorithms import ( _heap, _lexicographic, _steinhaus_johnson_trotter, @@ -15,7 +17,7 @@ ] -def generate(degree: int, algorithm: str = "lexicographic") -> Generator["Permutation", None, None]: +def generate(degree: int, algorithm: str = "lexicographic") -> Generator[Permutation, None, None]: """Generate all the permutations of the given degree based on the chosen algorithm. The method generates all the permutations of the given degree using the specified algorithm. @@ -45,37 +47,12 @@ def generate(degree: int, algorithm: str = "lexicographic") -> Generator["Permut Permutation(3, 1, 2) Permutation(3, 2, 1) """ - _check_algorithm_parameter(value=algorithm) + _check_algorithm_parameter(value=algorithm, supported=_SUPPORTED_ALGORITHM) _check_degree_parameter(value=degree) return _relevant_generator(algorithm=algorithm, degree=degree) -def _check_algorithm_parameter(value: str) -> None: - """Private method to check the value provided for the parameter `algorithm`. - - Recall that the parameter `algorithm` must be a string present in the list _SUPPORTED_ALGORITHM - """ - if isinstance(value, str) is False: - raise TypeError(f"The parameter `algorithm` must be of type string, but {type(value)} was provided.") - if value not in _SUPPORTED_ALGORITHM: - raise ValueError( - f"The given algorithm ({value}) is not supported. \n " - f"Here, a list of supported algorithm for generations of permutations {_SUPPORTED_ALGORITHM}." - ) - - -def _check_degree_parameter(value: int) -> None: - """Private method to check the value provided for the parameter `degree`. - - Recall that the parameter `degree` must be a non-negative integer different from zero. - """ - if isinstance(value, int) is False: - raise TypeError(f"The parameter `degree` must be of type int, but {type(value)} was provided.") - if value < 1: - raise ValueError(f"The parameter `degree` must be a non-zero positive integer, but {value} was provided.") - - -def _relevant_generator(algorithm: str, degree: int) -> Generator["Permutation", None, None]: +def _relevant_generator(algorithm: str, degree: int) -> Generator[Permutation, None, None]: """Private method to pick the correct algorithm for generating permutations.""" if algorithm == "lexicographic": return _lexicographic(degree=degree, start=list(range(1, degree + 1))) diff --git a/symmetria/generators/random/__init__.py b/symmetria/generators/random/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/symmetria/generators/random/_algorithms.py b/symmetria/generators/random/_algorithms.py new file mode 100644 index 0000000..1153846 --- /dev/null +++ b/symmetria/generators/random/_algorithms.py @@ -0,0 +1,33 @@ +from random import randint, shuffle +from typing import List, Generator + +from symmetria import Permutation + + +def _random_shuffle(permutation: List[int]) -> Permutation: + """Private method to generate a random permutation using the random module of Python.""" + shuffle(permutation) + return Permutation(*permutation) + + +def _random_shuffle_generator(degree: int) -> Generator[Permutation, None, None]: + """Private method to generate random permutations using the random module of Python.""" + permutation = list(range(1, degree + 1)) + while True: + yield _random_shuffle(permutation=permutation) + + +def _fisher_yates_shuffle(permutation: List[int]) -> Permutation: + """Private method to generate a random permutation using the Fisher-Yates shuffle.""" + n = len(permutation) + for i in range(n - 1, 0, -1): + j = randint(0, i) + permutation[i], permutation[j] = permutation[j], permutation[i] + return Permutation(*permutation) + + +def _fisher_yates_shuffle_generator(degree: int) -> Generator[Permutation, None, None]: + """Private method to generate random permutations using the Fisher-Yates shuffle.""" + permutation = list(range(1, degree + 1)) + while True: + yield _fisher_yates_shuffle(permutation=permutation) diff --git a/symmetria/generators/random/api.py b/symmetria/generators/random/api.py new file mode 100644 index 0000000..a975001 --- /dev/null +++ b/symmetria/generators/random/api.py @@ -0,0 +1,94 @@ +from typing import List, Generator + +from symmetria import Permutation +from symmetria.generators._validators import _check_degree_parameter, _check_algorithm_parameter +from symmetria.generators.random._algorithms import ( + _random_shuffle, + _fisher_yates_shuffle, + _random_shuffle_generator, + _fisher_yates_shuffle_generator, +) + +__all__ = ["random", "random_generator"] + +_SUPPORTED_ALGORITHM: List[str] = [ + "random", + "fisher-yates", +] + + +def random(degree: int, algorithm: str = "random") -> Permutation: + """Generate a random permutation of the given degree based on the chosen algorithm. + + The method generate a random permutation of the given degree using the specified algorithm. + + :param degree: The degree of the permutation to be generated. Must be a non-zero positive integer. + :type degree: int + :param algorithm: The algorithm to use for generating permutations. + Default is ``random``. + :type algorithm: str, optional + + :return: A permutation object. + :rtype: Permutation + + :raises ValueError: If the algorithm is not supported or the degree is invalid. + + :examples: + >>> import symmetria + ... + >>> permutation = symmetria.random(degree=3, algorithm="fisher-yates") + """ + _check_algorithm_parameter(value=algorithm, supported=_SUPPORTED_ALGORITHM) + _check_degree_parameter(value=degree) + return _relevant_random_permutation(algorithm=algorithm, degree=degree) + + +def _relevant_random_permutation(algorithm: str, degree: int) -> Permutation: + """Private method to pick the correct algorithm to generate the permutation.""" + if algorithm == "random": + return _random_shuffle(permutation=list(range(1, degree + 1))) + elif algorithm == "fisher-yates": + return _fisher_yates_shuffle(permutation=list(range(1, degree + 1))) + + +def random_generator(degree: int, algorithm: str = "random") -> Generator[Permutation, None, None]: + """Generate random permutations of the given degree based on the chosen algorithm. + + The method generates random permutations of the given degree using the specified algorithm. + + :param degree: The degree of the permutations to be generated. Must be a non-zero positive integer. + :type degree: int + :param algorithm: The algorithm to use for generating permutations. + Default is ``random``. + :type algorithm: str, optional + + :return: An infinite generator yielding permutations. + :rtype: Generator[Permutation, None, None] + + :raises ValueError: If the algorithm is not supported or the degree is invalid. + + :examples: + >>> import symmetria + ... + >>> permutations = symmetria.random_generator(degree=3, algorithm="fisher-yates") + """ + _check_algorithm_parameter(value=algorithm, supported=_SUPPORTED_ALGORITHM) + _check_degree_parameter(value=degree) + return _relevant_random_generator(algorithm=algorithm, degree=degree) + + +def _relevant_random_generator(algorithm: str, degree: int) -> Generator[Permutation, None, None]: + """Private method to pick the correct algorithm for generating random permutations.""" + if algorithm == "random": + return _random_shuffle_generator(degree=degree) + elif algorithm == "fisher-yates": + return _fisher_yates_shuffle_generator(degree=degree) + + +if __name__ == "__main__": + print(random(3, "fisher-yates").rep()) + for idx, p in enumerate(random_generator(12, "fisher-yates")): + print(p.rep()) + + if idx == 10: + break