From e228261a6be0f42ad72fb7f62fc270773494fbef Mon Sep 17 00:00:00 2001 From: mhostetter Date: Tue, 24 Jan 2023 19:42:35 -0500 Subject: [PATCH] Preserve intellisense autocompletion and brief docstring of patched `Poly` methods --- src/galois/_polys/__init__.py | 12 +++---- src/galois/_polys/_constructors.py | 46 ++++++++++++++++++++++++ src/galois/_polys/_factor.py | 45 +++++++++++------------ src/galois/_polys/_functions.py | 16 ++++++--- src/galois/_polys/_irreducible.py | 14 ++++---- src/galois/_polys/_poly.py | 58 ++++++++++++++++++++++++++++++ src/galois/_polys/_primitive.py | 26 +++++++------- src/galois/_polys/_search.py | 17 +++++---- 8 files changed, 175 insertions(+), 59 deletions(-) create mode 100644 src/galois/_polys/_constructors.py diff --git a/src/galois/_polys/__init__.py b/src/galois/_polys/__init__.py index 1cfae5420..b402ea72a 100644 --- a/src/galois/_polys/__init__.py +++ b/src/galois/_polys/__init__.py @@ -1,6 +1,7 @@ """ A subpackage containing arrays over Galois fields. """ +from . import _constructors from ._conway import * from ._factor import * from ._functions import * @@ -8,10 +9,7 @@ from ._poly import * from ._primitive import * -# pylint: disable=undefined-variable -Poly.square_free_factors = _factor.square_free_factors -Poly.distinct_degree_factors = _factor.distinct_degree_factors -Poly.equal_degree_factors = _factor.equal_degree_factors -Poly.factors = _factor.factors -Poly.is_irreducible = _irreducible.is_irreducible -Poly.is_primitive = _primitive.is_primitive +_constructors.POLY = Poly +_constructors.POLY_DEGREES = Poly.Degrees +_constructors.POLY_INT = Poly.Int +_constructors.POLY_RANDOM = Poly.Random diff --git a/src/galois/_polys/_constructors.py b/src/galois/_polys/_constructors.py new file mode 100644 index 000000000..71f4ad07a --- /dev/null +++ b/src/galois/_polys/_constructors.py @@ -0,0 +1,46 @@ +""" +A module containing polynomial constructors that will be monkey-patched to Poly(), Poly.Int(), Poly.Degrees(), +and Poly.Random() in polys/__init__.py. + +This is done to separate code into related modules without having circular imports with Poly(). +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Sequence, Type + +import numpy as np +from typing_extensions import Literal + +from .._domains import Array +from ..typing import ArrayLike + +if TYPE_CHECKING: + from ._poly import Poly + + +def POLY( + coeffs: ArrayLike, + field: Type[Array] | None = None, + order: Literal["desc", "asc"] = "desc", +) -> Poly: + raise NotImplementedError + + +def POLY_DEGREES( + degrees: Sequence[int] | np.ndarray, + coeffs: ArrayLike | None = None, + field: Type[Array] | None = None, +) -> Poly: + raise NotImplementedError + + +def POLY_INT(integer: int, field: Type[Array] | None = None) -> Poly: + raise NotImplementedError + + +def POLY_RANDOM( + degree: int, + seed: int | np.integer | np.random.Generator | None = None, + field: Type[Array] | None = None, +) -> Poly: + raise NotImplementedError diff --git a/src/galois/_polys/_factor.py b/src/galois/_polys/_factor.py index 8fb3d2c10..2040d0660 100644 --- a/src/galois/_polys/_factor.py +++ b/src/galois/_polys/_factor.py @@ -3,13 +3,17 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING + from .._helper import verify_isinstance +from . import _constructors from ._functions import gcd -from ._poly import Poly + +if TYPE_CHECKING: + from ._poly import Poly -# NOTE: This is a method of the Poly class. It will be added to the Poly class in `polys/__init__.py`. -def square_free_factors(self) -> tuple[list[Poly], list[int]]: +def _square_free_factors(self) -> tuple[list[Poly], list[int]]: r""" Factors the monic polynomial :math:`f(x)` into a product of square-free polynomials. @@ -68,7 +72,7 @@ def square_free_factors(self) -> tuple[list[Poly], list[int]]: field = self.field p = field.characteristic - one = Poly.One(field=field) + one = _constructors.POLY([1], field=field) factors_ = [] multiplicities = [] @@ -95,8 +99,8 @@ def square_free_factors(self) -> tuple[list[Poly], list[int]]: degrees = [degree // p for degree in d.nonzero_degrees] # The inverse Frobenius automorphism of the coefficients coeffs = d.nonzero_coeffs ** (field.characteristic ** (field.degree - 1)) - delta = Poly.Degrees(degrees, coeffs=coeffs, field=field) # The p-th root of d(x) - f, m = square_free_factors(delta) + delta = _constructors.POLY_DEGREES(degrees, coeffs=coeffs, field=field) # The p-th root of d(x) + f, m = _square_free_factors(delta) factors_.extend(f) multiplicities.extend([mi * p for mi in m]) @@ -106,8 +110,7 @@ def square_free_factors(self) -> tuple[list[Poly], list[int]]: return list(factors_), list(multiplicities) -# NOTE: This is a method of the Poly class. It will be added to the Poly class in `polys/__init__.py`. -def distinct_degree_factors(self) -> tuple[list[Poly], list[int]]: +def _distinct_degree_factors(self) -> tuple[list[Poly], list[int]]: r""" Factors the monic, square-free polynomial :math:`f(x)` into a product of polynomials whose irreducible factors all have the same degree. @@ -184,8 +187,8 @@ def distinct_degree_factors(self) -> tuple[list[Poly], list[int]]: field = self.field q = field.order n = self.degree - one = Poly.One(field=field) - x = Poly.Identity(field=field) + one = _constructors.POLY([1], field=field) + x = _constructors.POLY([1, 0], field=field) factors_ = [] degrees = [] @@ -211,8 +214,7 @@ def distinct_degree_factors(self) -> tuple[list[Poly], list[int]]: return factors_, degrees -# NOTE: This is a method of the Poly class. It will be added to the Poly class in `polys/__init__.py`. -def equal_degree_factors(self, degree: int) -> list[Poly]: +def _equal_degree_factors(self, degree: int) -> list[Poly]: r""" Factors the monic, square-free polynomial :math:`f(x)` of degree :math:`rd` into a product of :math:`r` irreducible factors with degree :math:`d`. @@ -282,11 +284,11 @@ def equal_degree_factors(self, degree: int) -> list[Poly]: field = self.field q = field.order r = self.degree // degree - one = Poly.One(field) + one = _constructors.POLY([1], field=field) factors_ = [self] while len(factors_) < r: - h = Poly.Random(degree, field=field) + h = _constructors.POLY_RANDOM(degree, field=field) g = gcd(self, h) if g == one: g = pow(h, (q**degree - 1) // 2, self) - one @@ -307,8 +309,7 @@ def equal_degree_factors(self, degree: int) -> list[Poly]: return factors_ -# NOTE: This is a method of the Poly class. It will be added to the Poly class in `polys/__init__.py`. -def factors(self) -> tuple[list[Poly], list[int]]: +def _factors(self) -> tuple[list[Poly], list[int]]: r""" Computes the irreducible factors of the non-constant, monic polynomial :math:`f(x)`. @@ -327,11 +328,11 @@ def factors(self) -> tuple[list[Poly], list[int]]: Steps: 1. Apply the Square-Free Factorization algorithm to factor the monic polynomial into square-free - polynomials. See :func:`~Poly.square_free_factors`. + polynomials. See :func:`~Poly.square_free_factors`. 2. Apply the Distinct-Degree Factorization algorithm to factor each square-free polynomial into a product - of factors with the same degree. See :func:`~Poly.distinct_degree_factors`. + of factors with the same degree. See :func:`~Poly.distinct_degree_factors`. 3. Apply the Equal-Degree Factorization algorithm to factor the product of factors of equal degree into - their irreducible factors. See :func:`~Poly.equal_degree_factors`. + their irreducible factors. See :func:`~Poly.equal_degree_factors`. References: - Hachenberger, D. and Jungnickel, D. Topics in Galois Fields. Algorithm 6.1.7. @@ -374,15 +375,15 @@ def factors(self) -> tuple[list[Poly], list[int]]: factors_, multiplicities = [], [] # Step 1: Find all the square-free factors - sf_factors, sf_multiplicities = square_free_factors(self) + sf_factors, sf_multiplicities = _square_free_factors(self) # Step 2: Find all the factors with distinct degree for sf_factor, sf_multiplicity in zip(sf_factors, sf_multiplicities): - df_factors, df_degrees = distinct_degree_factors(sf_factor) + df_factors, df_degrees = _distinct_degree_factors(sf_factor) # Step 3: Find all the irreducible factors with degree d for df_factor, df_degree in zip(df_factors, df_degrees): - f = equal_degree_factors(df_factor, df_degree) + f = _equal_degree_factors(df_factor, df_degree) factors_.extend(f) multiplicities.extend([sf_multiplicity] * len(f)) diff --git a/src/galois/_polys/_functions.py b/src/galois/_polys/_functions.py index 6253d5dbf..b60228d96 100644 --- a/src/galois/_polys/_functions.py +++ b/src/galois/_polys/_functions.py @@ -3,10 +3,16 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING + from .._domains import Array from .._helper import export, verify_isinstance +from . import _constructors from ._dense import lagrange_poly_jit -from ._poly import Poly + +if TYPE_CHECKING: + from ._poly import Poly + ############################################################################### # Divisibility @@ -40,8 +46,8 @@ def egcd(a: Poly, b: Poly) -> tuple[Poly, Poly, Poly]: raise ValueError(f"Polynomials `a` and `b` must be over the same Galois field, not {a.field} and {b.field}.") field = a.field - zero = Poly.Zero(field) - one = Poly.One(field) + zero = _constructors.POLY([0], field=field) + one = _constructors.POLY([1], field=field) r2, r1 = a, b s2, s1 = one, zero @@ -69,7 +75,7 @@ def lcm(*args: Poly) -> Poly: """ field = args[0].field - lcm_ = Poly.One(field) + lcm_ = _constructors.POLY([1], field=field) for arg in args: if not arg.field == field: raise ValueError( @@ -89,7 +95,7 @@ def prod(*args: Poly) -> Poly: """ field = args[0].field - prod_ = Poly.One(field) + prod_ = _constructors.POLY([1], field=field) for arg in args: if not arg.field == field: raise ValueError( diff --git a/src/galois/_polys/_irreducible.py b/src/galois/_polys/_irreducible.py index 2ba7b6e3b..251901867 100644 --- a/src/galois/_polys/_irreducible.py +++ b/src/galois/_polys/_irreducible.py @@ -4,15 +4,15 @@ from __future__ import annotations import functools -from typing import Iterator +from typing import TYPE_CHECKING, Iterator from typing_extensions import Literal from .._domains import _factory from .._helper import export, verify_isinstance from .._prime import factors, is_prime_power +from . import _constructors from ._functions import gcd -from ._poly import Poly from ._search import ( _deterministic_search, _deterministic_search_fixed_terms, @@ -21,10 +21,12 @@ _random_search_fixed_terms, ) +if TYPE_CHECKING: + from ._poly import Poly + -# NOTE: This is a method of the Poly class. It will be added to the Poly class in `polys/__init__.py`. @functools.lru_cache(maxsize=8192) -def is_irreducible(self) -> bool: +def _is_irreducible(self) -> bool: r""" Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(p^m)` is irreducible. @@ -98,10 +100,10 @@ def is_irreducible(self) -> bool: field = self.field q = field.order m = self.degree - x = Poly.Identity(field) + x = _constructors.POLY([1, 0], field=field) primes, _ = factors(m) - h0 = Poly.Identity(field) + h0 = _constructors.POLY([1, 0], field=field) n0 = 0 for ni in sorted([m // pi for pi in primes]): # The GCD of f(x) and (x^(q^(m/pi)) - x) must be 1 for f(x) to be irreducible, where pi are the diff --git a/src/galois/_polys/_poly.py b/src/galois/_polys/_poly.py index 2af910806..3ff037af0 100644 --- a/src/galois/_polys/_poly.py +++ b/src/galois/_polys/_poly.py @@ -21,6 +21,14 @@ sparse_poly_to_str, str_to_sparse_poly, ) +from ._factor import ( + _distinct_degree_factors, + _equal_degree_factors, + _factors, + _square_free_factors, +) +from ._irreducible import _is_irreducible +from ._primitive import _is_primitive # Values were obtained by running scripts/sparse_poly_performance_test.py SPARSE_VS_DENSE_POLY_FACTOR = 0.00_125 # 1.25% density @@ -767,6 +775,40 @@ def roots(self, multiplicity=False): multiplicities = np.array([_root_multiplicity(self, root) for root in roots]) return roots, multiplicities + def square_free_factors(self) -> tuple[list[Poly], list[int]]: + r""" + Factors the monic polynomial :math:`f(x)` into a product of square-free polynomials. + """ + return _square_free_factors(self) + + square_free_factors.__doc__ = _square_free_factors.__doc__ + + def distinct_degree_factors(self) -> tuple[list[Poly], list[int]]: + r""" + Factors the monic, square-free polynomial :math:`f(x)` into a product of polynomials whose irreducible factors + all have the same degree. + """ + return _distinct_degree_factors(self) + + distinct_degree_factors.__doc__ = _distinct_degree_factors.__doc__ + + def equal_degree_factors(self, degree: int) -> list[Poly]: + r""" + Factors the monic, square-free polynomial :math:`f(x)` of degree :math:`rd` into a product of :math:`r` + irreducible factors with degree :math:`d`. + """ + return _equal_degree_factors(self, degree) + + equal_degree_factors.__doc__ = _equal_degree_factors.__doc__ + + def factors(self) -> tuple[list[Poly], list[int]]: + r""" + Computes the irreducible factors of the non-constant, monic polynomial :math:`f(x)`. + """ + return _factors(self) + + factors.__doc__ = _factors.__doc__ + def derivative(self, k: int = 1) -> Poly: r""" Computes the :math:`k`-th formal derivative :math:`\frac{d^k}{dx^k} f(x)` of the polynomial :math:`f(x)`. @@ -849,6 +891,22 @@ def derivative(self, k: int = 1) -> Poly: return p_prime + def is_irreducible(self) -> bool: + r""" + Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(p^m)` is irreducible. + """ + return _is_irreducible(self) + + is_irreducible.__doc__ = _is_irreducible.__doc__ + + def is_primitive(self) -> bool: + r""" + Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` is primitive. + """ + return _is_primitive(self) + + is_primitive.__doc__ = _is_primitive.__doc__ + def is_square_free(self) -> bool: r""" Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` is square-free. diff --git a/src/galois/_polys/_primitive.py b/src/galois/_polys/_primitive.py index 5b4ac4c0b..7104c9e64 100644 --- a/src/galois/_polys/_primitive.py +++ b/src/galois/_polys/_primitive.py @@ -4,15 +4,15 @@ from __future__ import annotations import functools -from typing import Iterator +from typing import TYPE_CHECKING, Iterator from typing_extensions import Literal from .._domains import _factory from .._helper import export, verify_isinstance from .._prime import factors, is_prime, is_prime_power -from ._irreducible import is_irreducible -from ._poly import Poly +from . import _constructors +from ._irreducible import _is_irreducible from ._search import ( _deterministic_search, _deterministic_search_fixed_terms, @@ -21,10 +21,12 @@ _random_search_fixed_terms, ) +if TYPE_CHECKING: + from ._poly import Poly + -# NOTE: This is a method of the Poly class. It will be added to the Poly class in `polys/__init__.py`. @functools.lru_cache(maxsize=8192) -def is_primitive(self) -> bool: +def _is_primitive(self) -> bool: r""" Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` is primitive. @@ -73,24 +75,24 @@ def is_primitive(self) -> bool: if self.field.order == 2 and self.degree == 1: # There is only one primitive polynomial in GF(2) - return self == Poly([1, 1]) + return self == _constructors.POLY([1, 1]) if self.coeffs[-1] == 0: # A primitive polynomial cannot have zero constant term # TODO: Why isn't f(x) = x primitive? It's irreducible and passes the primitivity tests. return False - if not is_irreducible(self): + if not _is_irreducible(self): # A polynomial must be irreducible to be primitive return False field = self.field q = field.order m = self.degree - one = Poly.One(field) + one = _constructors.POLY([1], field=field) primes, _ = factors(q**m - 1) - x = Poly.Identity(field) + x = _constructors.POLY([1, 0], field=field) for ki in sorted([(q**m - 1) // pi for pi in primes]): # f(x) must not divide (x^((q^m - 1)/pi) - 1) for f(x) to be primitive, where pi are the prime factors # of q**m - 1. @@ -418,14 +420,14 @@ def matlab_primitive_poly(characteristic: int, degree: int) -> Poly: # But for some reason, there are three exceptions. I can't determine why. if characteristic == 2 and degree == 7: # Not the lexicographically-first of x^7 + x + 1. - return Poly.Degrees([7, 3, 0]) + return _constructors.POLY_DEGREES([7, 3, 0]) if characteristic == 2 and degree == 14: # Not the lexicographically-first of x^14 + x^5 + x^3 + x + 1. - return Poly.Degrees([14, 10, 6, 1, 0]) + return _constructors.POLY_DEGREES([14, 10, 6, 1, 0]) if characteristic == 2 and degree == 16: # Not the lexicographically-first of x^16 + x^5 + x^3 + x^2 + 1. - return Poly.Degrees([16, 12, 3, 1, 0]) + return _constructors.POLY_DEGREES([16, 12, 3, 1, 0]) return primitive_poly(characteristic, degree) diff --git a/src/galois/_polys/_search.py b/src/galois/_polys/_search.py index a81407c4a..3df826aed 100644 --- a/src/galois/_polys/_search.py +++ b/src/galois/_polys/_search.py @@ -8,12 +8,15 @@ import functools import random -from typing import Callable, Iterable, Iterator, Type +from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Type import numpy as np from .._domains import Array, _factory -from ._poly import Poly +from . import _constructors + +if TYPE_CHECKING: + from ._poly import Poly @functools.lru_cache(maxsize=8192) @@ -29,7 +32,7 @@ def _deterministic_search( (either 'is_irreducible()' or 'is_primitive()'). This function returns `None` if no such polynomial exists. """ for element in range(start, stop, step): - poly = Poly.Int(element, field=field) + poly = _constructors.POLY_INT(element, field=field) if getattr(poly, test)(): return poly @@ -76,7 +79,7 @@ def _deterministic_search_fixed_terms_recursive( """ if terms == 0: # There are no more terms, yield the polynomial. - poly = Poly.Degrees(degrees, coeffs, field=field) + poly = _constructors.POLY_DEGREES(degrees, coeffs, field=field) if getattr(poly, test)(): yield poly elif terms == 1: @@ -113,7 +116,7 @@ def _random_search(order: int, degree: int, test: str) -> Iterator[Poly]: while True: integer = random.randint(start, stop - 1) - poly = Poly.Int(integer, field=field) + poly = _constructors.POLY_INT(integer, field=field) if getattr(poly, test)(): yield poly @@ -133,7 +136,7 @@ def _random_search_fixed_terms( if terms == 1: # The x^m term is always 1. If there's only one term, then the x^m is the polynomial. - poly = Poly.Degrees([degree], [1], field=field) + poly = _constructors.POLY_DEGREES([degree], [1], field=field) if getattr(poly, test)(): yield poly else: @@ -144,7 +147,7 @@ def _random_search_fixed_terms( x0_coeff = np.random.randint(1, field.order) degrees = (degree, *mid_degrees, 0) coeffs = (1, *mid_coeffs, x0_coeff) - poly = Poly.Degrees(degrees, coeffs, field=field) + poly = _constructors.POLY_DEGREES(degrees, coeffs, field=field) if getattr(poly, test)(): yield poly