From fc526791101834db785ecea9ee4daa3a54c9d1e1 Mon Sep 17 00:00:00 2001 From: mhostetter Date: Wed, 30 Mar 2022 09:03:14 -0400 Subject: [PATCH] Refactor `primitive_roots()` to return an iterator - Also add `method` kwarg to `primitive_root()` --- docs/tutorials/intro-to-prime-fields.rst | 4 +- galois/_modular.py | 615 +++++++++++++---------- tests/test_modular.py | 108 ++-- 3 files changed, 417 insertions(+), 310 deletions(-) diff --git a/docs/tutorials/intro-to-prime-fields.rst b/docs/tutorials/intro-to-prime-fields.rst index d0dcf7822..a41533d95 100644 --- a/docs/tutorials/intro-to-prime-fields.rst +++ b/docs/tutorials/intro-to-prime-fields.rst @@ -443,7 +443,7 @@ There are multiple primitive elements of any finite field. All primitive element @suppress GF7.display("int") - galois.primitive_roots(7) + list(galois.primitive_roots(7)) GF7.primitive_elements g = GF7(5); g @@ -454,7 +454,7 @@ There are multiple primitive elements of any finite field. All primitive element @suppress GF7.display("power") - galois.primitive_roots(7) + list(galois.primitive_roots(7)) GF7.primitive_elements g = GF7(5); g diff --git a/galois/_modular.py b/galois/_modular.py index 9ffb1f413..fca6093f5 100644 --- a/galois/_modular.py +++ b/galois/_modular.py @@ -1,5 +1,8 @@ +import functools import math -from typing import List, Optional +import random +from typing import List, Optional, Iterator +from typing_extensions import Literal import numpy as np @@ -9,7 +12,7 @@ __all__ = [ "totatives", "euler_phi", "carmichael_lambda", "is_cyclic", - "is_primitive_root", "primitive_root", "primitive_roots", + "primitive_root", "primitive_roots", "is_primitive_root" ] @@ -254,83 +257,112 @@ def is_cyclic(n: int) -> bool: Examples -------- - The elements of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` are the positive integers less than :math:`n` that are coprime with :math:`n`. - For example, :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}`. + .. tab-set:: - .. ipython:: python + .. tab-item:: n = 14 - # n is of type 2*p^e, which is cyclic - n = 14 - galois.is_cyclic(n) - Znx = set(galois.totatives(n)); Znx - phi = galois.euler_phi(n); phi - len(Znx) == phi + The elements of :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}` are the totatives of :math:`14`. - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = galois.is_primitive_root(a, n) - print("Element: {:2d}, Span: {:<20}, Primitive root: {}".format(a, str(span), primitive_root)) + .. ipython:: python - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + n = 14 + Znx = galois.totatives(n); Znx - # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n - len(roots) == galois.euler_phi(phi) + The Euler totient :math:`\phi(n)` function counts the totatives of :math:`n`, which is equivalent to the order + of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}`. - A counterexample is :math:`n = 15 = 3 \cdot 5`, which doesn't fit the condition for cyclicness. - :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`. Since the group is not cyclic, it has no primitive roots. + .. ipython:: python - .. ipython:: python + phi = galois.euler_phi(n); phi + len(Znx) == phi - # n is of type p1^e1 * p2^e2, which is not cyclic - n = 15 - galois.is_cyclic(n) - Znx = set(galois.totatives(n)); Znx - phi = galois.euler_phi(n); phi - len(Znx) == phi + Since :math:`14` is of the form :math:`2p^k`, the multiplicative group :math:`(\mathbb{Z}/14\mathbb{Z}){^\times}` is cyclic, + meaning there exists at least one element that generates the group by its powers. - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = galois.is_primitive_root(a, n) - print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root)) + .. ipython:: python - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + galois.is_cyclic(n) - # Note the max order of any element is 4, not 8, which is Carmichael's lambda function - galois.carmichael_lambda(n) + Find the smallest primitive root modulo :math:`14`. Observe that the powers of :math:`g` uniquely represent each element + in :math:`(\mathbb{Z}/14\mathbb{Z}){^\times}`. - For prime :math:`n`, a primitive root modulo :math:`n` is also a primitive element of the Galois field :math:`\mathrm{GF}(n)`. A - primitive element is a generator of the multiplicative group :math:`\mathrm{GF}(p)^{\times} = \{1, 2, \dots, p-1\} = \{1, g, g^2, \dots, g^{\phi(n)-1}\}`. + .. ipython:: python - .. ipython:: python + g = galois.primitive_root(n); g + [pow(g, i, n) for i in range(0, phi)] - # n is of type p, which is cyclic - n = 7 - galois.is_cyclic(n) - Znx = set(galois.totatives(n)); Znx - phi = galois.euler_phi(n); phi - len(Znx) == phi + Find the largest primitive root modulo :math:`14`. Observe that the powers of :math:`g` also uniquely represent each element + in :math:`(\mathbb{Z}/14\mathbb{Z}){^\times}`, although in a different order. + + .. ipython:: python + + g = galois.primitive_root(n, method="max"); g + [pow(g, i, n) for i in range(0, phi)] + + .. tab-item:: n = 15 + + A non-cyclic group is :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`. + + .. ipython:: python + + n = 15 + Znx = galois.totatives(n); Znx + phi = galois.euler_phi(n); phi + + Since :math:`15` is not of the form :math:`2`, :math:`4`, :math:`p^k`, or :math:`2p^k`, the multiplicative group :math:`(\mathbb{Z}/15\mathbb{Z}){^\times}` + is not cyclic, meaning no elements exist whose powers generate the group. + + .. ipython:: python + + galois.is_cyclic(n) + + Below, every element is tested to see if it spans the group. + + .. ipython:: python + + for a in Znx: + span = set([pow(a, i, n) for i in range(0, phi)]) + primitive_root = span == set(Znx) + print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root)) + + The Carmichael :math:`\lambda(n)` function finds the maximum multiplicative order of any element, which is + :math:`4` and not :math:`8`. + + .. ipython:: python + + galois.carmichael_lambda(n) + + Observe that no primitive roots modulo :math:`15` exist and a `RuntimeError` is raised. + + .. ipython:: python + :okexcept: + + galois.primitive_root(n) + + .. tab-item:: Prime fields + + For prime :math:`n`, a primitive root modulo :math:`n` is also a primitive element of the Galois field :math:`\mathrm{GF}(n)`. - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = galois.is_primitive_root(a, n) - print("Element: {:2d}, Span: {:<18}, Primitive root: {}".format(a, str(span), primitive_root)) + .. ipython:: python - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + n = 31 + galois.is_cyclic(n) - # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n - len(roots) == galois.euler_phi(phi) + A primitive element is a generator of the multiplicative group :math:`\mathrm{GF}(p)^{\times} = \{1, 2, \dots, p-1\} = \{1, g, g^2, \dots, g^{\phi(n)-1}\}`. + + .. ipython:: python + + GF = galois.GF(n) + galois.primitive_root(n) + GF.primitive_element + + The number of primitive roots/elements is :math:`\phi(\phi(n))`. + + .. ipython:: python + + list(galois.primitive_roots(n)) + GF.primitive_elements + galois.euler_phi(galois.euler_phi(n)) """ if not isinstance(n, (int, np.integer)): raise TypeError(f"Argument `n` must be an integer, not {type(n)}.") @@ -356,220 +388,211 @@ def is_cyclic(n: int) -> bool: @set_module("galois") -def is_primitive_root(g: int, n: int) -> bool: +def primitive_root(n: int, start: int = 1, stop: Optional[int] = None, method: Literal["min", "max", "random"] = "min") -> int: r""" - Determines if :math:`g` is a primitive root modulo :math:`n`. + Finds a primitive root modulo :math:`n` in the range `[start, stop)`. Parameters ---------- - g - A positive integer that may be a primitive root modulo :math:`n`. n A positive integer. + start + Starting value (inclusive) in the search for a primitive root. + stop + Stopping value (exclusive) in the search for a primitive root. The default is `None` which corresponds to :math:`n`. + method + The search method for finding the primitive root. Returns ------- : - `True` if :math:`g` is a primitive root modulo :math:`n`. + A primitive root modulo :math:`n` in the specified range. + + Raises + ------ + RuntimeError + If no primitive roots exist in the specified range. + + See Also + -------- + primitive_roots, is_primitive_root, is_cyclic, totatives, euler_phi, carmichael_lambda Notes ----- - The integer :math:`g` is a primitive root modulo :math:`n` if the totatives of :math:`n`, the positive integers - :math:`1 \le a < n` that are coprime with :math:`n`, can be generated by powers of :math:`g`. + The integer :math:`g` is a primitive root modulo :math:`n` if the totatives of :math:`n` can be generated by the + powers of :math:`g`. The totatives of :math:`n` are the positive integers in :math:`[1, n)` that are coprime with :math:`n`. Alternatively said, :math:`g` is a primitive root modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative - group of integers modulo :math:`n`, - - .. math:: - (\mathbb{Z}/n\mathbb{Z}){^\times} = \{1, g, g^2, \dots, g^{\phi(n)-1}\} - + group of integers modulo :math:`n` :math:`(\mathbb{Z}/n\mathbb{Z}){^\times} = \{1, g, g^2, \dots, g^{\phi(n)-1}\}`, where :math:`\phi(n)` is order of the group. If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`. + References + ---------- + * V. Shoup. Searching for primitive roots in finite fields. https://www.ams.org/journals/mcom/1992-58-197/S0025-5718-1992-1106981-9/S0025-5718-1992-1106981-9.pdf + * L. K. Hua. On the least primitive root of a prime. https://www.ams.org/journals/bull/1942-48-10/S0002-9904-1942-07767-6/S0002-9904-1942-07767-6.pdf + * http://www.numbertheory.org/courses/MP313/lectures/lecture7/page1.html + Examples -------- - .. ipython:: python + .. tab-set:: - galois.is_primitive_root(2, 7) - galois.is_primitive_root(3, 7) - galois.primitive_roots(7) - """ - if not isinstance(g, (int, np.integer)): - raise TypeError(f"Argument `g` must be an integer, not {type(g)}.") - if not isinstance(n, (int, np.integer)): - raise TypeError(f"Argument `n` must be an integer, not {type(n)}.") - if not n > 0: - raise ValueError(f"Argument `n` must be a positive integer, not {n}.") - if not 0 < g < n: - raise ValueError(f"Argument `g` must be a positive integer less than `n`, not {g}.") + .. tab-item:: n = 14 - if n == 2: - # Euler totient of 2 is 1. We cannot compute the prime factorization of 1. There is only one - # primitive root modulo 2 and it's 1. - return g == 1 + The elements of :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}` are the totatives of :math:`14`. - phi = euler_phi(n) # Number of non-zero elements in the multiplicative group Z/nZ - primes, _ = factors(phi) + .. ipython:: python - return pow(g, phi, n) == 1 and all(pow(g, phi // p, n) != 1 for p in primes) + n = 14 + Znx = galois.totatives(n); Znx + The Euler totient :math:`\phi(n)` function counts the totatives of :math:`n`, which is equivalent to the order + of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}`. -@set_module("galois") -def primitive_root(n: int, start: int = 1, stop: Optional[int] = None, reverse: bool = False) -> Optional[int]: - r""" - Finds the smallest primitive root modulo :math:`n`. + .. ipython:: python - Parameters - ---------- - n - A positive integer. - start - Starting value (inclusive) in the search for a primitive root. The default is 1. The resulting primitive - root, if found, will be :math:`\textrm{start} \le g < \textrm{stop}`. - stop - Stopping value (exclusive) in the search for a primitive root. The default is `None` which corresponds to :math:`n`. - The resulting primitive root, if found, will be :math:`\textrm{start} \le g < \textrm{stop}`. - reverse - Search for a primitive root in reverse order, i.e. find the largest primitive root first. Default is `False`. + phi = galois.euler_phi(n); phi + len(Znx) == phi - Returns - ------- - : - The smallest primitive root modulo :math:`n`. Returns `None` if no primitive roots exist. + Since :math:`14` is of the form :math:`2p^k`, the multiplicative group :math:`(\mathbb{Z}/14\mathbb{Z}){^\times}` is cyclic, + meaning there exists at least one element that generates the group by its powers. - Notes - ----- - The integer :math:`g` is a primitive root modulo :math:`n` if the totatives of :math:`n`, the positive integers - :math:`1 \le a < n` that are coprime with :math:`n`, can be generated by powers of :math:`g`. + .. ipython:: python - Alternatively said, :math:`g` is a primitive root modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative - group of integers modulo :math:`n`, + galois.is_cyclic(n) - .. math:: - (\mathbb{Z}/n\mathbb{Z}){^\times} = \{1, g, g^2, \dots, g^{\phi(n)-1}\} + Find the smallest primitive root modulo :math:`14`. Observe that the powers of :math:`g` uniquely represent each element + in :math:`(\mathbb{Z}/14\mathbb{Z}){^\times}`. - where :math:`\phi(n)` is order of the group. + .. ipython:: python - If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`. + g = galois.primitive_root(n); g + [pow(g, i, n) for i in range(0, phi)] - References - ---------- - * V. Shoup. Searching for primitive roots in finite fields. https://www.ams.org/journals/mcom/1992-58-197/S0025-5718-1992-1106981-9/S0025-5718-1992-1106981-9.pdf - * L. K. Hua. On the least primitive root of a prime. https://www.ams.org/journals/bull/1942-48-10/S0002-9904-1942-07767-6/S0002-9904-1942-07767-6.pdf - * http://www.numbertheory.org/courses/MP313/lectures/lecture7/page1.html + Find the largest primitive root modulo :math:`14`. Observe that the powers of :math:`g` also uniquely represent each element + in :math:`(\mathbb{Z}/14\mathbb{Z}){^\times}`, although in a different order. - Examples - -------- - The elements of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` are the positive integers less than :math:`n` that are coprime with :math:`n`. - For example, :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}`. + .. ipython:: python - .. ipython:: python + g = galois.primitive_root(n, method="max"); g + [pow(g, i, n) for i in range(0, phi)] - # n is of type 2*p^k, which is cyclic - n = 14 - galois.is_cyclic(n) + .. tab-item:: n = 15 - # The congruence class coprime with n - Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx + A non-cyclic group is :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`. - # Euler's totient function counts the "totatives", positive integers coprime with n - phi = galois.euler_phi(n); phi + .. ipython:: python - len(Znx) == phi + n = 15 + Znx = galois.totatives(n); Znx + phi = galois.euler_phi(n); phi - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = span == Znx - print("Element: {:2d}, Span: {:<20}, Primitive root: {}".format(a, str(span), primitive_root)) + Since :math:`15` is not of the form :math:`2`, :math:`4`, :math:`p^k`, or :math:`2p^k`, the multiplicative group :math:`(\mathbb{Z}/15\mathbb{Z}){^\times}` + is not cyclic, meaning no elements exist whose powers generate the group. - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + .. ipython:: python - # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n - len(roots) == galois.euler_phi(phi) + galois.is_cyclic(n) - A counterexample is :math:`n = 15 = 3 \cdot 5`, which doesn't fit the condition for cyclicness. - :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`. + Below, every element is tested to see if it spans the group. - .. ipython:: python + .. ipython:: python - # n is of type p1^k1 * p2^k2, which is not cyclic - n = 15 - galois.is_cyclic(n) + for a in Znx: + span = set([pow(a, i, n) for i in range(0, phi)]) + primitive_root = span == set(Znx) + print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root)) - # The congruence class coprime with n - Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx + The Carmichael :math:`\lambda(n)` function finds the maximum multiplicative order of any element, which is + :math:`4` and not :math:`8`. - # Euler's totient function counts the "totatives", positive integers coprime with n - phi = galois.euler_phi(n); phi + .. ipython:: python - len(Znx) == phi + galois.carmichael_lambda(n) - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = span == Znx - print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root)) + Observe that no primitive roots modulo :math:`15` exist and a `RuntimeError` is raised. - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + .. ipython:: python + :okexcept: - # Note the max order of any element is 4, not 8, which is Carmichael's lambda function - galois.carmichael_lambda(n) + galois.primitive_root(n) - The algorithm is also efficient for very large :math:`n`. + .. tab-item:: Very large n - .. ipython:: python + The algorithm is also efficient for very large :math:`n`. - n = 1000000000000000035000061 - galois.primitive_root(n) + .. ipython:: python + + n = 1000000000000000035000061 + phi = galois.euler_phi(n); phi + + Find the smallest, the largest, and a random primitive root modulo :math:`n`. + + .. ipython:: python + + galois.primitive_root(n) + galois.primitive_root(n, method="max") + galois.primitive_root(n, method="random") """ + if n in [1, 2]: + return n - 1 + + stop = n if stop is None else stop + if not isinstance(n, (int, np.integer)): + raise TypeError(f"Argument `n` must be an integer, not {type(n)}.") + if not isinstance(start, (int, np.integer)): + raise TypeError(f"Argument `start` must be an integer, not {type(start)}.") + if not isinstance(stop, (int, np.integer)): + raise TypeError(f"Argument `stop` must be an integer, not {type(stop)}.") + if not 1 <= start < stop <= n: + raise ValueError(f"Arguments must satisfy `1 <= start < stop <= n`, not `1 <= {start} < {stop} <= {n}`.") + if not method in ["min", "max", "random"]: + raise ValueError(f"Argument `method` must be in ['min', 'max', 'random'], not {method!r}.") + try: - return next(_primitive_roots(n, start=start, stop=stop, reverse=reverse)) - except StopIteration: - return None + if method == "min": + return next(primitive_roots(n, start, stop=stop)) + elif method == "max": + return next(primitive_roots(n, start, stop=stop, reverse=True)) + else: + return _primitive_root_random_search(n, start, stop) + except StopIteration as e: + raise RuntimeError(f"No primitive roots modulo {n} exist in the range [{start}, {stop}).") from e @set_module("galois") -def primitive_roots(n: int, start: int = 1, stop: Optional[int] = None, reverse: bool = False) -> List[int]: +def primitive_roots(n: int, start: int = 1, stop: Optional[int] = None, reverse: bool = False) -> Iterator[int]: r""" - Finds all primitive roots modulo :math:`n`. + Iterates through all primitive roots modulo :math:`n` in the range `[start, stop)`. Parameters ---------- n A positive integer. start - Starting value (inclusive) in the search for a primitive root. The default is 1. The resulting primitive - roots, if found, will be :math:`\textrm{start} \le x < \textrm{stop}`. + Starting value (inclusive) in the search for a primitive root. The default is 1. stop - Stopping value (exclusive) in the search for a primitive root. The default is `None` which corresponds to `n`. - The resulting primitive roots, if found, will be :math:`\textrm{start} \le x < \textrm{stop}`. + Stopping value (exclusive) in the search for a primitive root. The default is `None` which corresponds to :math:`n`. reverse - List all primitive roots in descending order, i.e. largest to smallest. Default is `False`. + Indicates to return the primitive roots from largest to smallest. The default is `False`. Returns ------- : - All the positive primitive :math:`n`-th roots of unity, :math:`x`. + An iterator over the primitive roots modulo :math:`n` in the specified range. + + See Also + -------- + primitive_root, is_primitive_root, is_cyclic, totatives, euler_phi, carmichael_lambda Notes ----- - The integer :math:`g` is a primitive root modulo :math:`n` if the totatives of :math:`n`, the positive integers - :math:`1 \le a < n` that are coprime with :math:`n`, can be generated by powers of :math:`g`. + The integer :math:`g` is a primitive root modulo :math:`n` if the totatives of :math:`n` can be generated by the + powers of :math:`g`. The totatives of :math:`n` are the positive integers in :math:`[1, n)` that are coprime with :math:`n`. Alternatively said, :math:`g` is a primitive root modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative - group of integers modulo :math:`n`, - - .. math:: - (\mathbb{Z}/n\mathbb{Z}){^\times} = \{1, g, g^2, \dots, g^{\phi(n)-1}\} - + group of integers modulo :math:`n` :math:`(\mathbb{Z}/n\mathbb{Z}){^\times} = \{1, g, g^2, \dots, g^{\phi(n)-1}\}`, where :math:`\phi(n)` is order of the group. If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`. @@ -577,76 +600,57 @@ def primitive_roots(n: int, start: int = 1, stop: Optional[int] = None, reverse: References ---------- * V. Shoup. Searching for primitive roots in finite fields. https://www.ams.org/journals/mcom/1992-58-197/S0025-5718-1992-1106981-9/S0025-5718-1992-1106981-9.pdf + * L. K. Hua. On the least primitive root of a prime. https://www.ams.org/journals/bull/1942-48-10/S0002-9904-1942-07767-6/S0002-9904-1942-07767-6.pdf * http://www.numbertheory.org/courses/MP313/lectures/lecture7/page1.html Examples -------- - The elements of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` are the positive integers less than :math:`n` that are coprime with :math:`n`. - For example, :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}`. + .. tab-set:: - .. ipython:: python + .. tab-item:: Return full list - # n is of type 2*p^k, which is cyclic - n = 14 - galois.is_cyclic(n) + All primitive roots modulo :math:`31`. You may also use :func:`tuple` on the returned generator. - # The congruence class coprime with n - Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx + .. ipython:: python - # Euler's totient function counts the "totatives", positive integers coprime with n - phi = galois.euler_phi(n); phi + list(galois.primitive_roots(31)) - len(Znx) == phi + There are no primitive roots modulo :math:`30`. - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = span == Znx - print("Element: {:2d}, Span: {:<20}, Primitive root: {}".format(a, str(span), primitive_root)) + .. ipython:: python - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + list(galois.primitive_roots(30)) - # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n - len(roots) == galois.euler_phi(phi) + .. tab-item:: Use generator - A counterexample is :math:`n = 15 = 3 \cdot 5`, which doesn't fit the condition for cyclicness. - :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`. + Show the each primitive root modulo :math:`22` generates the multiplicative group :math:`(\mathbb{Z}/22\mathbb{Z}){^\times}`. - .. ipython:: python + .. ipython:: python - # n is of type p1^k1 * p2^k2, which is not cyclic - n = 15 - galois.is_cyclic(n) + n = 22 + Znx = galois.totatives(n); Znx + phi = galois.euler_phi(n); phi + for root in galois.primitive_roots(22): + span = set(pow(root, i, n) for i in range(0, phi)) + print(f"Element: {root:>2}, Span: {span}") - # The congruence class coprime with n - Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx + Find the three largest primitive roots modulo :math:`31` in reversed order. - # Euler's totient function counts the "totatives", positive integers coprime with n - phi = galois.euler_phi(n); phi + .. ipython:: python - len(Znx) == phi + generator = galois.primitive_roots(31, reverse=True); generator + [next(generator) for _ in range(3)] - # The primitive roots are the elements in Znx that multiplicatively generate the group - for a in Znx: - span = set([pow(a, i, n) for i in range(1, phi + 1)]) - primitive_root = span == Znx - print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root)) + Loop over all the primitive roots in reversed order, only finding them as needed. The search cost for the roots that would + have been found after the `break` condition is never incurred. - # Find the smallest primitive root - galois.primitive_root(n) - # Find all primitive roots - roots = galois.primitive_roots(n); roots + .. ipython:: python - # Note the max order of any element is 4, not 8, which is Carmichael's lambda function - galois.carmichael_lambda(n) + for root in galois.primitive_roots(31, reverse=True): + print(root) + if root % 7 == 0: # Arbitrary early exit condition + break """ - return list(_primitive_roots(n, start=start, stop=stop, reverse=reverse)) - - -def _primitive_roots(n, start=1, stop=None, reverse=False): if n in [1, 2]: yield n - 1 return @@ -663,25 +667,122 @@ def _primitive_roots(n, start=1, stop=None, reverse=False): if not 1 <= start < stop <= n: raise ValueError(f"Arguments must satisfy `1 <= start < stop <= n`, not `1 <= {start} < {stop} <= {n}`.") + # If the multiplicative group (Z/nZ)* is not cyclic, then it has no multiplicative generators if not is_cyclic(n): return - n = int(n) # Needed for the pow() function - phi = euler_phi(n) # Number of non-zero elements in the multiplicative group Z/nZ - primes, _ = factors(phi) - + phi = euler_phi(n) # Number of non-zero elements in the multiplicative group (Z/nZ)* if phi == n - 1 or n % 2 == 1: - # For prime n or odd n, must test all elements - possible_roots = range(start, stop) + # For prime n or odd n, we must test all elements + step = 1 else: - # For even n, only have to test odd elements + # For even n, we only have to test odd elements if start % 2 == 0: start += 1 # Make start odd - possible_roots = range(start, stop, 2) + step = 2 if reverse: - possible_roots = reversed(possible_roots) + start, stop, step = stop - 1, start - 1, -1 + + while True: + root = _primitive_root_deterministic_search(n, start, stop, step) + if root is not None: + start = root + step + yield root + else: + break + + +# @functools.lru_cache(maxsize=4096) +def _primitive_root_deterministic_search(n, start, stop, step) -> Optional[int]: + """ + Searches for a primitive root in the range using the specified deterministic method. + """ + for root in range(start, stop, step): + if _is_primitive_root(root, n): + return root + + return None + + +def _primitive_root_random_search(n, start, stop) -> int: + """ + Searches for a random primitive root. + """ + i = 0 + while True: + root = random.randint(start, stop - 1) + if _is_primitive_root(root, n): + return root + + i += 1 + if i > 2*(stop - start): + # A primitive root should have been found given 2*N tries + raise StopIteration + + +@set_module("galois") +def is_primitive_root(g: int, n: int) -> bool: + r""" + Determines if :math:`g` is a primitive root modulo :math:`n`. - for r in possible_roots: - if pow(r, phi, n) == 1 and all(pow(r, phi // p, n) != 1 for p in primes): - yield r + Parameters + ---------- + g + A positive integer. + n + A positive integer. + + Returns + ------- + : + `True` if :math:`g` is a primitive root modulo :math:`n`. + + Notes + ----- + The integer :math:`g` is a primitive root modulo :math:`n` if the totatives of :math:`n`, the positive integers + :math:`1 \le a < n` that are coprime with :math:`n`, can be generated by powers of :math:`g`. + + Alternatively said, :math:`g` is a primitive root modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative + group of integers modulo :math:`n`, + + .. math:: + (\mathbb{Z}/n\mathbb{Z}){^\times} = \{1, g, g^2, \dots, g^{\phi(n)-1}\} + + where :math:`\phi(n)` is order of the group. + + If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`. + + Examples + -------- + .. ipython:: python + + galois.is_primitive_root(2, 7) + galois.is_primitive_root(3, 7) + list(galois.primitive_roots(7)) + """ + if not isinstance(g, (int, np.integer)): + raise TypeError(f"Argument `g` must be an integer, not {type(g)}.") + if not isinstance(n, (int, np.integer)): + raise TypeError(f"Argument `n` must be an integer, not {type(n)}.") + if not n > 0: + raise ValueError(f"Argument `n` must be a positive integer, not {n}.") + if not 0 < g < n: + raise ValueError(f"Argument `g` must be a positive integer less than `n`, not {g}.") + + return _is_primitive_root(g, n) + + +def _is_primitive_root(g: int, n: int) -> bool: + """ + A private version of `is_primitive_root()` without type checking for internal use. + """ + if n == 2: + # Euler totient of 2 is 1. We cannot compute the prime factorization of 1. There is only one + # primitive root modulo 2 and it's 1. + return g == 1 + + phi = euler_phi(n) # Number of non-zero elements in the multiplicative group Z/nZ + primes, _ = factors(phi) + + return pow(g, phi, n) == 1 and all(pow(g, phi // p, n) != 1 for p in primes) diff --git a/tests/test_modular.py b/tests/test_modular.py index 53f8a220c..5ee58593d 100644 --- a/tests/test_modular.py +++ b/tests/test_modular.py @@ -14,7 +14,10 @@ def test_smallest_primitive_root(): ns = range(1, 101) roots = [0,1,2,3,2,5,3,None,2,3,2,None,2,3,None,None,3,5,2,None,None,7,5,None,2,7,2,None,2,None,3,None,None,3,None,None,2,3,None,None,6,None,3,None,None,5,5,None,3,3,None,None,2,5,None,None,None,3,2,None,2,3,None,None,None,None,2,None,None,None,7,None,5,5,None,None,None,None,3,None,2,7,2,None,None,3,None,None,3,None,None,None,None,5,None,None,5,3,None,None] for n, root in zip(ns, roots): - assert galois.primitive_root(n) == root + try: + assert galois.primitive_root(n) == root + except RuntimeError: + assert root is None def test_largest_primitive_root(): @@ -22,7 +25,10 @@ def test_largest_primitive_root(): ns = range(1, 101) roots = [0,1,2,3,3,5,5,None,5,7,8,None,11,5,None,None,14,11,15,None,None,19,21,None,23,19,23,None,27,None,24,None,None,31,None,None,35,33,None,None,35,None,34,None,None,43,45,None,47,47,None,None,51,47,None,None,None,55,56,None,59,55,None,None,None,None,63,None,None,None,69,None,68,69,None,None,None,None,77,None,77,75,80,None,None] for n, root in zip(ns, roots): - assert galois.primitive_root(n, reverse=True) == root + try: + assert galois.primitive_root(n, method="max") == root + except RuntimeError: + assert root is None def test_smallest_primitive_root_of_primes(): @@ -38,18 +44,16 @@ def test_largest_primitive_root_of_primes(): ns = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281] roots = [1,2,3,5,8,11,14,15,21,27,24,35,35,34,45,51,56,59,63,69,68,77,80,86,92,99,101,104,103,110,118,128,134,135,147,146,152,159,165,171,176,179,189,188,195,197,207,214,224,223,230,237,234,248,254,261,267,269,272] for n, root in zip(ns, roots): - assert galois.primitive_root(n, reverse=True) == root + assert galois.primitive_root(n, method="max") == root def test_primitive_root_exceptions(): with pytest.raises(TypeError): - galois.primitive_root(20.0) - with pytest.raises(TypeError): - galois.primitive_root(20, start=1.0) + galois.primitive_root(31.0) with pytest.raises(TypeError): - galois.primitive_root(20, stop=20.0) + galois.primitive_root(31, start=1.0) with pytest.raises(TypeError): - galois.primitive_root(20, reverse=1) + galois.primitive_root(31, stop=31.0) with pytest.raises(ValueError): galois.primitive_root(0) @@ -63,40 +67,42 @@ def test_primitive_root_exceptions(): galois.primitive_root(7, stop=1) with pytest.raises(ValueError): galois.primitive_root(7, start=6, stop=6) + with pytest.raises(ValueError): + galois.primitive_root(31, method="invalid") def test_primitive_roots(): # https://en.wikipedia.org/wiki/Primitive_root_modulo_n - assert galois.primitive_roots(1) == [0] - assert galois.primitive_roots(2) == [1] - assert galois.primitive_roots(3) == [2] - assert galois.primitive_roots(4) == [3] - assert galois.primitive_roots(5) == [2,3] - assert galois.primitive_roots(6) == [5] - assert galois.primitive_roots(7) == [3,5] - assert galois.primitive_roots(8) == [] - assert galois.primitive_roots(9) == [2,5] - assert galois.primitive_roots(10) == [3,7] - assert galois.primitive_roots(11) == [2,6,7,8] - assert galois.primitive_roots(12) == [] - assert galois.primitive_roots(13) == [2,6,7,11] - assert galois.primitive_roots(14) == [3,5] - assert galois.primitive_roots(15) == [] - assert galois.primitive_roots(16) == [] - assert galois.primitive_roots(17) == [3,5,6,7,10,11,12,14] - assert galois.primitive_roots(18) == [5,11] - assert galois.primitive_roots(19) == [2,3,10,13,14,15] - assert galois.primitive_roots(20) == [] - assert galois.primitive_roots(21) == [] - assert galois.primitive_roots(22) == [7,13,17,19] - assert galois.primitive_roots(23) == [5,7,10,11,14,15,17,19,20,21] - assert galois.primitive_roots(24) == [] - assert galois.primitive_roots(25) == [2,3,8,12,13,17,22,23] - assert galois.primitive_roots(26) == [7,11,15,19] - assert galois.primitive_roots(27) == [2,5,11,14,20,23] - assert galois.primitive_roots(28) == [] - assert galois.primitive_roots(29) == [2,3,8,10,11,14,15,18,19,21,26,27] - assert galois.primitive_roots(30) == [] + assert list(galois.primitive_roots(1)) == [0] + assert list(galois.primitive_roots(2)) == [1] + assert list(galois.primitive_roots(3)) == [2] + assert list(galois.primitive_roots(4)) == [3] + assert list(galois.primitive_roots(5)) == [2,3] + assert list(galois.primitive_roots(6)) == [5] + assert list(galois.primitive_roots(7)) == [3,5] + assert list(galois.primitive_roots(8)) == [] + assert list(galois.primitive_roots(9)) == [2,5] + assert list(galois.primitive_roots(10)) == [3,7] + assert list(galois.primitive_roots(11)) == [2,6,7,8] + assert list(galois.primitive_roots(12)) == [] + assert list(galois.primitive_roots(13)) == [2,6,7,11] + assert list(galois.primitive_roots(14)) == [3,5] + assert list(galois.primitive_roots(15)) == [] + assert list(galois.primitive_roots(16)) == [] + assert list(galois.primitive_roots(17)) == [3,5,6,7,10,11,12,14] + assert list(galois.primitive_roots(18)) == [5,11] + assert list(galois.primitive_roots(19)) == [2,3,10,13,14,15] + assert list(galois.primitive_roots(20)) == [] + assert list(galois.primitive_roots(21)) == [] + assert list(galois.primitive_roots(22)) == [7,13,17,19] + assert list(galois.primitive_roots(23)) == [5,7,10,11,14,15,17,19,20,21] + assert list(galois.primitive_roots(24)) == [] + assert list(galois.primitive_roots(25)) == [2,3,8,12,13,17,22,23] + assert list(galois.primitive_roots(26)) == [7,11,15,19] + assert list(galois.primitive_roots(27)) == [2,5,11,14,20,23] + assert list(galois.primitive_roots(28)) == [] + assert list(galois.primitive_roots(29)) == [2,3,8,10,11,14,15,18,19,21,26,27] + assert list(galois.primitive_roots(30)) == [] def test_number_of_primitive_roots(): @@ -104,7 +110,7 @@ def test_number_of_primitive_roots(): ns = list(range(1,92)) num_roots = [1,1,1,1,2,1,2,0,2,2,4,0,4,2,0,0,8,2,6,0,0,4,10,0,8,4,6,0,12,0,8,0,0,8,0,0,12,6,0,0,16,0,12,0,0,10,22,0,12,8,0,0,24,6,0,0,0,12,28,0,16,8,0,0,0,0,20,0,0,0,24,0,24,12,0,0,0,0,24,0,18,16,40,0,0,12,0,0,40,0,0] for n, num in zip(ns, num_roots): - assert len(galois.primitive_roots(n)) == num + assert len(list(galois.primitive_roots(n))) == num def test_number_of_primitive_roots_of_primes(): @@ -112,7 +118,7 @@ def test_number_of_primitive_roots_of_primes(): ns = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353] num_roots = [1,1,2,2,4,4,8,6,10,12,8,12,16,12,22,24,28,16,20,24,24,24,40,40,32,40,32,52,36,48,36,48,64,44,72,40,48,54,82,84,88,48,72,64,84,60,48,72,112,72,112,96,64,100,128,130,132,72,88,96,92,144,96,120,96,156,80,96,172,112] for n, num in zip(ns, num_roots): - assert len(galois.primitive_roots(n)) == num + assert len(list(galois.primitive_roots(n))) == num def test_sum_primitive_roots_of_primes(): @@ -130,7 +136,7 @@ def test_primitive_roots_are_generators(n): phi = galois.euler_phi(n) assert len(congruences) == phi - roots = galois.primitive_roots(n) + roots = list(galois.primitive_roots(n)) for root in roots: elements = [pow(root, i, n) for i in range(1, n)] assert set(congruences) == set(elements) @@ -140,26 +146,26 @@ def test_primitive_roots_are_generators(n): def test_primitive_roots_exceptions(): with pytest.raises(TypeError): - galois.primitive_roots(20.0) + next(galois.primitive_roots(31.0)) with pytest.raises(TypeError): - galois.primitive_roots(20, start=1.0) + next(galois.primitive_roots(31, start=1.0)) with pytest.raises(TypeError): - galois.primitive_roots(20, stop=20.0) + next(galois.primitive_roots(31, stop=31.0)) with pytest.raises(TypeError): - galois.primitive_roots(20, reverse=1) + next(galois.primitive_roots(31, reverse=1)) with pytest.raises(ValueError): - galois.primitive_roots(0) + next(galois.primitive_roots(0)) with pytest.raises(ValueError): - galois.primitive_roots(-2) + next(galois.primitive_roots(-2)) with pytest.raises(ValueError): - galois.primitive_roots(7, start=0) + next(galois.primitive_roots(7, start=0)) with pytest.raises(ValueError): - galois.primitive_roots(7, start=7) + next(galois.primitive_roots(7, start=7)) with pytest.raises(ValueError): - galois.primitive_roots(7, stop=1) + next(galois.primitive_roots(7, stop=1)) with pytest.raises(ValueError): - galois.primitive_roots(7, start=6, stop=6) + next(galois.primitive_roots(7, start=6, stop=6)) def test_is_primitive_root():