From 4c61dadd8e3594adb55a10fc06c4f98c8a1ac0ed Mon Sep 17 00:00:00 2001 From: mhostetter Date: Thu, 26 Jan 2023 12:11:55 -0500 Subject: [PATCH] Better solution for avoiding circular imports --- src/galois/_helper.py | 18 ++++++ src/galois/_polys/__init__.py | 7 --- src/galois/_polys/_constructors.py | 46 ---------------- src/galois/_polys/_factor.py | 88 +++++++++++++++++++++++------- src/galois/_polys/_functions.py | 15 ++--- src/galois/_polys/_irreducible.py | 16 +++--- src/galois/_polys/_poly.py | 80 +++++---------------------- src/galois/_polys/_primitive.py | 28 +++++----- src/galois/_polys/_search.py | 17 +++--- 9 files changed, 132 insertions(+), 183 deletions(-) delete mode 100644 src/galois/_polys/_constructors.py diff --git a/src/galois/_helper.py b/src/galois/_helper.py index e09c08081..b9cbacc4c 100644 --- a/src/galois/_helper.py +++ b/src/galois/_helper.py @@ -81,6 +81,24 @@ def export(obj): return obj +def method_of(class_): + """ + Monkey-patches the decorated function into the class as a method. The class should already have a stub method + that raises `NotImplementedError`. The docstring of the stub method is replaced with the docstring of the + decorated function. + + This is used to separate code into multiple files while still keeping the methods in the same class. + """ + + def decorator(func): + setattr(class_, func.__name__, func) + setattr(getattr(class_, func.__name__), "__doc__", func.__doc__) + + return func + + return decorator + + def extend_docstring(method, replace=None, docstring=""): """ A decorator to extend the docstring of `method` with the provided docstring. The decorator also finds diff --git a/src/galois/_polys/__init__.py b/src/galois/_polys/__init__.py index 3856e915d..7541b2e1f 100644 --- a/src/galois/_polys/__init__.py +++ b/src/galois/_polys/__init__.py @@ -1,16 +1,9 @@ """ A subpackage containing arrays over Galois fields. """ -from . import _constructors from ._conway import * from ._factor import * -from ._functions import * from ._irreducible import * from ._lagrange import * from ._poly import * from ._primitive import * - -_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 deleted file mode 100644 index 71f4ad07a..000000000 --- a/src/galois/_polys/_constructors.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -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 590f59af3..6c8a99766 100644 --- a/src/galois/_polys/_factor.py +++ b/src/galois/_polys/_factor.py @@ -3,17 +3,62 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING - -from .._helper import verify_isinstance -from . import _constructors +from .._helper import method_of, verify_isinstance from ._functions import gcd +from ._poly import Poly + +__all__ = [] + + +@method_of(Poly) +def is_square_free(f) -> bool: + r""" + Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` is square-free. + + .. info:: + + This is a method, not a property, to indicate this test is computationally expensive. + + Returns: + `True` if the polynomial is square-free. + + Notes: + A square-free polynomial :math:`f(x)` has no irreducible factors with multiplicity greater than one. + Therefore, its canonical factorization is + + .. math:: + f(x) = \prod_{i=1}^{k} g_i(x)^{e_i} = \prod_{i=1}^{k} g_i(x) . + + Examples: + Generate irreducible polynomials over :math:`\mathrm{GF}(3)`. + + .. ipython:: python + + GF = galois.GF(3) + f1 = galois.irreducible_poly(3, 3); f1 + f2 = galois.irreducible_poly(3, 4); f2 + + Determine if composite polynomials are square-free over :math:`\mathrm{GF}(3)`. + + .. ipython:: python + + (f1 * f2).is_square_free() + (f1**2 * f2).is_square_free() + """ + if not f.is_monic: + f //= f.coeffs[0] + + # Constant polynomials are square-free + if f.degree == 0: + return True + + _, multiplicities = square_free_factors(f) -if TYPE_CHECKING: - from ._poly import Poly + return multiplicities == [1] -def _square_free_factors(f: Poly) -> tuple[list[Poly], list[int]]: +@method_of(Poly) +def square_free_factors(f: Poly) -> tuple[list[Poly], list[int]]: r""" Factors the monic polynomial :math:`f(x)` into a product of square-free polynomials. @@ -72,7 +117,7 @@ def _square_free_factors(f: Poly) -> tuple[list[Poly], list[int]]: field = f.field p = field.characteristic - one = _constructors.POLY([1], field=field) + one = Poly([1], field=field) factors_ = [] multiplicities = [] @@ -100,8 +145,8 @@ def _square_free_factors(f: Poly) -> 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 = _constructors.POLY_DEGREES(degrees, coeffs=coeffs, field=field) # The p-th root of d(x) - g, m = _square_free_factors(delta) + delta = Poly.Degrees(degrees, coeffs=coeffs, field=field) # The p-th root of d(x) + g, m = square_free_factors(delta) factors_.extend(g) multiplicities.extend([mi * p for mi in m]) @@ -111,7 +156,8 @@ def _square_free_factors(f: Poly) -> tuple[list[Poly], list[int]]: return list(factors_), list(multiplicities) -def _distinct_degree_factors(f: Poly) -> tuple[list[Poly], list[int]]: +@method_of(Poly) +def distinct_degree_factors(f: Poly) -> 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. @@ -188,8 +234,8 @@ def _distinct_degree_factors(f: Poly) -> tuple[list[Poly], list[int]]: field = f.field q = field.order n = f.degree - one = _constructors.POLY([1], field=field) - x = _constructors.POLY([1, 0], field=field) + one = Poly([1], field=field) + x = Poly([1, 0], field=field) factors_ = [] degrees = [] @@ -215,7 +261,8 @@ def _distinct_degree_factors(f: Poly) -> tuple[list[Poly], list[int]]: return factors_, degrees -def _equal_degree_factors(f: Poly, degree: int) -> list[Poly]: +@method_of(Poly) +def equal_degree_factors(f: Poly, 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`. @@ -283,11 +330,11 @@ def _equal_degree_factors(f: Poly, degree: int) -> list[Poly]: field = f.field q = field.order r = f.degree // degree - one = _constructors.POLY([1], field=field) + one = Poly([1], field=field) factors_ = [f] while len(factors_) < r: - h = _constructors.POLY_RANDOM(degree, field=field) + h = Poly.Random(degree, field=field) g = gcd(f, h) if g == one: g = pow(h, (q**degree - 1) // 2, f) - one @@ -308,7 +355,8 @@ def _equal_degree_factors(f: Poly, degree: int) -> list[Poly]: return factors_ -def _factors(f) -> tuple[list[Poly], list[int]]: +@method_of(Poly) +def factors(f) -> tuple[list[Poly], list[int]]: r""" Computes the irreducible factors of the non-constant, monic polynomial :math:`f(x)`. @@ -374,15 +422,15 @@ def _factors(f) -> tuple[list[Poly], list[int]]: factors_, multiplicities = [], [] # Step 1: Find all the square-free factors - sf_factors, sf_multiplicities = _square_free_factors(f) + sf_factors, sf_multiplicities = square_free_factors(f) # 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 41fcc1eea..fa4a08bd8 100644 --- a/src/galois/_polys/_functions.py +++ b/src/galois/_polys/_functions.py @@ -3,12 +3,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING - -from . import _constructors - -if TYPE_CHECKING: - from ._poly import Poly +from ._poly import Poly def gcd(a: Poly, b: Poly) -> Poly: @@ -38,8 +33,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 = _constructors.POLY([0], field=field) - one = _constructors.POLY([1], field=field) + zero = Poly([0], field=field) + one = Poly([1], field=field) r2, r1 = a, b s2, s1 = one, zero @@ -67,7 +62,7 @@ def lcm(*args: Poly) -> Poly: """ field = args[0].field - lcm_ = _constructors.POLY([1], field=field) + lcm_ = Poly([1], field=field) for arg in args: if not arg.field == field: raise ValueError( @@ -87,7 +82,7 @@ def prod(*args: Poly) -> Poly: """ field = args[0].field - prod_ = _constructors.POLY([1], field=field) + prod_ = 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 fe904e88c..e42d3aa2a 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 TYPE_CHECKING, Iterator +from typing import Iterator from typing_extensions import Literal from .._domains import _factory -from .._helper import export, verify_isinstance +from .._helper import export, method_of, 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,12 +21,10 @@ _random_search_fixed_terms, ) -if TYPE_CHECKING: - from ._poly import Poly - +@method_of(Poly) @functools.lru_cache(maxsize=8192) -def _is_irreducible(f: Poly) -> bool: +def is_irreducible(f: Poly) -> bool: r""" Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(p^m)` is irreducible. @@ -101,10 +99,10 @@ def _is_irreducible(f: Poly) -> bool: field = f.field q = field.order m = f.degree - x = _constructors.POLY([1, 0], field=field) + x = Poly([1, 0], field=field) primes, _ = factors(m) - h0 = _constructors.POLY([1, 0], field=field) + h0 = 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 3ff037af0..2f546e1af 100644 --- a/src/galois/_polys/_poly.py +++ b/src/galois/_polys/_poly.py @@ -21,14 +21,6 @@ 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 @@ -779,35 +771,31 @@ 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__ + # Will be monkey-patched in `_factor.py` + raise NotImplementedError 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__ + # Will be monkey-patched in `_factor.py` + raise NotImplementedError 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__ + # Will be monkey-patched in `_factor.py` + raise NotImplementedError 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__ + # Will be monkey-patched in `_factor.py` + raise NotImplementedError def derivative(self, k: int = 1) -> Poly: r""" @@ -895,62 +883,22 @@ 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__ + # Will be monkey-patched in `_irreducible.py` + raise NotImplementedError 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__ + # Will be monkey-patched in `_primitive.py` + raise NotImplementedError def is_square_free(self) -> bool: r""" Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` is square-free. - - .. info:: - - This is a method, not a property, to indicate this test is computationally expensive. - - Returns: - `True` if the polynomial is square-free. - - Notes: - A square-free polynomial :math:`f(x)` has no irreducible factors with multiplicity greater than one. - Therefore, its canonical factorization is - - .. math:: - f(x) = \prod_{i=1}^{k} g_i(x)^{e_i} = \prod_{i=1}^{k} g_i(x) . - - Examples: - Generate irreducible polynomials over :math:`\mathrm{GF}(3)`. - - .. ipython:: python - - GF = galois.GF(3) - f1 = galois.irreducible_poly(3, 3); f1 - f2 = galois.irreducible_poly(3, 4); f2 - - Determine if composite polynomials are square-free over :math:`\mathrm{GF}(3)`. - - .. ipython:: python - - (f1 * f2).is_square_free() - (f1**2 * f2).is_square_free() """ - if not self.is_monic: - self //= self.coeffs[0] - - # Constant polynomials are square-free - if self.degree == 0: - return True - - _, multiplicities = self.square_free_factors() - - return multiplicities == [1] + # Will be monkey-patched in `_factor.py` + raise NotImplementedError ############################################################################### # Overridden dunder methods diff --git a/src/galois/_polys/_primitive.py b/src/galois/_polys/_primitive.py index a35534357..823147236 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 TYPE_CHECKING, Iterator +from typing import Iterator from typing_extensions import Literal from .._domains import _factory -from .._helper import export, verify_isinstance +from .._helper import export, method_of, verify_isinstance from .._prime import factors, is_prime, is_prime_power -from . import _constructors -from ._irreducible import _is_irreducible +from ._irreducible import is_irreducible +from ._poly import Poly from ._search import ( _deterministic_search, _deterministic_search_fixed_terms, @@ -21,12 +21,10 @@ _random_search_fixed_terms, ) -if TYPE_CHECKING: - from ._poly import Poly - +@method_of(Poly) @functools.lru_cache(maxsize=8192) -def _is_primitive(f: Poly) -> bool: +def is_primitive(f: Poly) -> bool: r""" Determines whether the polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` is primitive. @@ -75,24 +73,24 @@ def _is_primitive(f: Poly) -> bool: if f.field.order == 2 and f.degree == 1: # There is only one primitive polynomial in GF(2) - return f == _constructors.POLY([1, 1]) + return f == Poly([1, 1]) if f.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(f): + if not is_irreducible(f): # A polynomial must be irreducible to be primitive return False field = f.field q = field.order m = f.degree - one = _constructors.POLY([1], field=field) + one = Poly([1], field=field) primes, _ = factors(q**m - 1) - x = _constructors.POLY([1, 0], field=field) + x = 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. @@ -420,14 +418,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 _constructors.POLY_DEGREES([7, 3, 0]) + return 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 _constructors.POLY_DEGREES([14, 10, 6, 1, 0]) + return 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 _constructors.POLY_DEGREES([16, 12, 3, 1, 0]) + return 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 b53ade894..6c66771bf 100644 --- a/src/galois/_polys/_search.py +++ b/src/galois/_polys/_search.py @@ -8,15 +8,12 @@ import functools import random -from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Sequence, Type +from typing import Callable, Iterable, Iterator, Sequence, Type import numpy as np from .._domains import Array, _factory -from . import _constructors - -if TYPE_CHECKING: - from ._poly import Poly +from ._poly import Poly @functools.lru_cache(maxsize=8192) @@ -32,7 +29,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 = _constructors.POLY_INT(element, field=field) + poly = Poly.Int(element, field=field) if getattr(poly, test)(): return poly @@ -79,7 +76,7 @@ def _deterministic_search_fixed_terms_recursive( """ if terms == 0: # There are no more terms, yield the polynomial. - poly = _constructors.POLY_DEGREES(degrees, coeffs, field=field) + poly = Poly.Degrees(degrees, coeffs, field=field) if getattr(poly, test)(): yield poly elif terms == 1: @@ -116,7 +113,7 @@ def _random_search(order: int, degree: int, test: str) -> Iterator[Poly]: while True: integer = random.randint(start, stop - 1) - poly = _constructors.POLY_INT(integer, field=field) + poly = Poly.Int(integer, field=field) if getattr(poly, test)(): yield poly @@ -136,7 +133,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 = _constructors.POLY_DEGREES([degree], [1], field=field) + poly = Poly.Degrees([degree], [1], field=field) if getattr(poly, test)(): yield poly else: @@ -147,7 +144,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 = _constructors.POLY_DEGREES(degrees, coeffs, field=field) + poly = Poly.Degrees(degrees, coeffs, field=field) if getattr(poly, test)(): yield poly