diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da9cfd767..6b186efc5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,4 +11,5 @@ repos: rev: v0.4.7 hooks: - id: ruff + args: [--fix, --exit-non-zero-on-fix] - id: ruff-format diff --git a/README.md b/README.md index 889654002..872cf1e96 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The new ufuncs are written in pure Python and [just-in-time compiled](https://nu ## Features -- Supports all [Galois fields](https://mhostetter.github.io/galois/latest/api/galois.GF/) $\mathrm{GF}(p^m)$, even arbitrarily-large fields! +- Supports all [Galois fields](https://mhostetter.github.io/galois/latest/api/galois.GF/) $\mathrm{GF}(p^m)$, even arbitrarily large fields! - [**Faster**](https://mhostetter.github.io/galois/latest/performance/prime-fields/) than native NumPy! `GF(x) * GF(y)` is faster than `(x * y) % p` for $\mathrm{GF}(p)$. - Seamless integration with NumPy -- normal NumPy functions work on [`FieldArray`](https://mhostetter.github.io/galois/latest/api/galois.FieldArray/)s. - Linear algebra over finite fields using normal [`np.linalg`](https://mhostetter.github.io/galois/latest/basic-usage/array-arithmetic/#linear-algebra) functions. diff --git a/docs/basic-usage/array-arithmetic.rst b/docs/basic-usage/array-arithmetic.rst index c71fe4b4e..dc7c7031d 100644 --- a/docs/basic-usage/array-arithmetic.rst +++ b/docs/basic-usage/array-arithmetic.rst @@ -292,7 +292,7 @@ Advanced arithmetic :collapsible: The Discrete Fourier Transform (DFT) of size $n$ over the finite field $\mathrm{GF}(p^m)$ exists when - there exists a primitive $n$-th root of unity. This occurs when $n\ |\ p^m - 1$. + there exists a primitive $n$-th root of unity. This occurs when $n \mid p^m - 1$. .. ipython-with-reprs:: int,poly,power @@ -310,7 +310,7 @@ Advanced arithmetic :collapsible: The inverse Discrete Fourier Transform (DFT) of size $n$ over the finite field $\mathrm{GF}(p^m)$ - exists when there exists a primitive $n$-th root of unity. This occurs when $n\ |\ p^m - 1$. + exists when there exists a primitive $n$-th root of unity. This occurs when $n \mid p^m - 1$. .. ipython-with-reprs:: int,poly,power diff --git a/docs/index.rst b/docs/index.rst index 1e7af191a..531607d2a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,7 +40,7 @@ calculation (for memory savings). Features -------- -- Supports all Galois fields $\mathrm{GF}(p^m)$, even arbitrarily-large fields! +- Supports all Galois fields $\mathrm{GF}(p^m)$, even arbitrarily large fields! - **Faster** than native NumPy! `GF(x) * GF(y)` is faster than `(x * y) % p` for $\mathrm{GF}(p)$. - Seamless integration with NumPy -- normal NumPy functions work on :obj:`~galois.FieldArray` instances. - Linear algebra over finite fields using normal :obj:`numpy.linalg` functions. diff --git a/docs/release-notes/v0.0.md b/docs/release-notes/v0.0.md index 8da4e5d0f..129427b46 100644 --- a/docs/release-notes/v0.0.md +++ b/docs/release-notes/v0.0.md @@ -307,7 +307,7 @@ Poly(1, GF(2)) - Allow polynomial comparison with integers and field scalars. Now `galois.Poly([0]) == 0` and `galois.Poly([0]) == GF(0)` return `True` rather than raising `TypeError`. - Support testing 0-degree polynomials for irreducibility and primitivity. - Extend `crt()` to work over non co-prime moduli. -- Extend `prev_prime()` and `next_prime()` to work over arbitrarily-large inputs. +- Extend `prev_prime()` and `next_prime()` to work over arbitrarily large inputs. - Allow negative integer inputs to `primes()`, `is_prime()`, `is_composite()`, `is_prime_power()`, `is_perfect_power()`, `is_square_free()`, `is_smooth()`, and `is_powersmooth()`. - Fix various type hinting errors. - Various other bug fixes. @@ -715,8 +715,8 @@ Poly(1, GF(2)) - Moved `galois.square_free_factorization()` function into `Poly.square_free_factors()` method. ([#362](https://github.com/mhostetter/galois/pull/362)) - Moved `galois.distinct_degree_factorization()` function into `Poly.distinct_degree_factors()` method. ([#362](https://github.com/mhostetter/galois/pull/362)) - Moved `galois.equal_degree_factorization()` function into `Poly.equal_degree_factors()` method. ([#362](https://github.com/mhostetter/galois/pull/362)) -- Moved `galois.is_irreducible()` function into `Poly.is_irreducible()` method. This is a method, not property, to indicate it is a computationally-expensive operation. ([#362](https://github.com/mhostetter/galois/pull/362)) -- Moved `galois.is_primitive()` function into `Poly.is_primitive()` method. This is a method, not property, to indicate it is a computationally-expensive operation. ([#362](https://github.com/mhostetter/galois/pull/362)) +- Moved `galois.is_irreducible()` function into `Poly.is_irreducible()` method. This is a method, not property, to indicate it is a computationally expensive operation. ([#362](https://github.com/mhostetter/galois/pull/362)) +- Moved `galois.is_primitive()` function into `Poly.is_primitive()` method. This is a method, not property, to indicate it is a computationally expensive operation. ([#362](https://github.com/mhostetter/galois/pull/362)) - Moved `galois.is_monic()` function into `Poly.is_monic` property. ([#362](https://github.com/mhostetter/galois/pull/362)) ### Changes @@ -742,7 +742,7 @@ Poly(1, GF(2)) - Added `galois.get_printoptions()` function to return the current package-wide printing options. This is the equivalent of `np.get_printoptions()`. ([#363](https://github.com/mhostetter/galois/pull/363)) - Added `galois.printoptions()` context manager to modify printing options inside of a `with` statement. This is the equivalent of `np.printoptions()`. ([#363](https://github.com/mhostetter/galois/pull/363)) - Added a separate `Poly.factors()` method, in addition to the polymorphic `galois.factors()`. ([#362](https://github.com/mhostetter/galois/pull/362)) -- Added a separate `Poly.is_square_free()` method, in addition to the polymorphic `galois.is_square_free()`. This is a method, not property, to indicate it is a computationally-expensive operation. ([#362](https://github.com/mhostetter/galois/pull/362)) +- Added a separate `Poly.is_square_free()` method, in addition to the polymorphic `galois.is_square_free()`. This is a method, not property, to indicate it is a computationally expensive operation. ([#362](https://github.com/mhostetter/galois/pull/362)) - Fixed a bug (believed to be introduced in v0.0.26) where `Poly.degree` occasionally returned `np.int64` instead of `int`. This could cause overflow in certain large integer operations (e.g., computing $q^m$ when determining if a degree-$m$ polynomial over $\mathrm{GF}(q)$ is irreducible). When the integer overflowed, this created erroneous results. ([#360](https://github.com/mhostetter/galois/issues/360), [#361](https://github.com/mhostetter/galois/pull/361)) - Increased code coverage. @@ -756,7 +756,7 @@ Poly(1, GF(2)) ### Changes -- Added support for NumPy 1.22 with Numba 0.55.2. This allows users to upgrade NumPy and avoid recently-discovered vulnerabilities [CVE-2021-34141](https://nvd.nist.gov/vuln/detail/CVE-2021-34141), [CVE-2021-41496](https://nvd.nist.gov/vuln/detail/CVE-2021-41496), and [CVE-2021-41495](https://nvd.nist.gov/vuln/detail/CVE-2021-41495). ([#366](https://github.com/mhostetter/galois/pull/366)) +- Added support for NumPy 1.22 with Numba 0.55.2. This allows users to upgrade NumPy and avoid recently discovered vulnerabilities [CVE-2021-34141](https://nvd.nist.gov/vuln/detail/CVE-2021-34141), [CVE-2021-41496](https://nvd.nist.gov/vuln/detail/CVE-2021-41496), and [CVE-2021-41495](https://nvd.nist.gov/vuln/detail/CVE-2021-41495). ([#366](https://github.com/mhostetter/galois/pull/366)) - Made `FieldArray.repr_table()` more compact. ([#367](https://github.com/mhostetter/galois/pull/367)) ```ipython In [2]: GF = galois.GF(3**3) diff --git a/docs/release-notes/v0.3.md b/docs/release-notes/v0.3.md index 2d6235245..ed0784991 100644 --- a/docs/release-notes/v0.3.md +++ b/docs/release-notes/v0.3.md @@ -173,7 +173,7 @@ tocdepth: 2 Poly(x^9 + 3x^2 + 4, GF(7)) ``` - Added a database of binary irreducible polynomials with degrees less than 10,000. These polynomials are - lexicographically-first and have the minimum number of non-zero terms. The database is accessed in + lexicographically first and have the minimum number of non-zero terms. The database is accessed in `irreducible_poly()` when `terms="min"` and `method="min"`. ([#462](https://github.com/mhostetter/galois/pull/462)) ```ipython In [1]: import galois diff --git a/pyproject.toml b/pyproject.toml index 0186afb48..068a6f7e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,6 +118,13 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "F403"] +[tool.mypy] +plugins = "numpy.typing.mypy_plugin" + +[[tool.mypy.overrides]] +module = ["numba.*"] +ignore_missing_imports = true + [tool.pytest.ini_options] minversion = "6.2" addopts = "-s --showlocals" diff --git a/src/galois/_codes/_bch.py b/src/galois/_codes/_bch.py index ab2a5eac5..4c4efe18d 100644 --- a/src/galois/_codes/_bch.py +++ b/src/galois/_codes/_bch.py @@ -4,21 +4,18 @@ from __future__ import annotations -from typing import Type, overload +from typing import Any, Type, overload -import numba import numpy as np -from numba import int64 +import numpy.typing as npt from typing_extensions import Literal -from .._domains._function import Function from .._fields import GF2, Field, FieldArray from .._helper import export, extend_docstring, verify_isinstance, verify_issubclass -from .._lfsr import berlekamp_massey_jit from .._math import ilog from .._polys import Poly, matlab_primitive_poly -from .._polys._dense import evaluate_elementwise_jit, roots_jit from ..typing import ArrayLike, ElementLike +from ._bm_decoder import berlekamp_decode_jit from ._cyclic import _CyclicCode @@ -473,7 +470,7 @@ def encode(self, message: ArrayLike, output: Literal["codeword", "parity"] = "co bch.detect(c) """, ) - def detect(self, codeword: ArrayLike) -> bool | np.ndarray: + def detect(self, codeword: ArrayLike) -> bool | npt.NDArray: return super().detect(codeword) @overload @@ -490,7 +487,7 @@ def decode( codeword: ArrayLike, output: Literal["message", "codeword"] = "message", errors: Literal[True] = True, - ) -> tuple[FieldArray, int | np.ndarray]: ... + ) -> tuple[FieldArray, int | npt.NDArray]: ... @extend_docstring( _CyclicCode.decode, @@ -650,11 +647,11 @@ def decode( np.array_equal(d, m) """, ) - def decode(self, codeword, output="message", errors=False): + def decode(self, codeword: Any, output: Any = "message", errors: Any = False) -> Any: return super().decode(codeword, output=output, errors=errors) - def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, np.ndarray]: - func = bch_decode_jit(self.field, self.extension_field) + def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, npt.NDArray]: + func = berlekamp_decode_jit(self.field, self.extension_field) dec_codeword, N_errors = func(codeword, self.n, int(self.alpha), self.c, self.roots) dec_codeword = dec_codeword.view(self.field) return dec_codeword, N_errors @@ -709,6 +706,7 @@ def extension_field(self) -> Type[FieldArray]: """ return self._extension_field + @property @extend_docstring( _CyclicCode.n, {}, @@ -729,10 +727,10 @@ def extension_field(self) -> Type[FieldArray]: bch.n """, ) - @property def n(self) -> int: return super().n + @property @extend_docstring( _CyclicCode.k, {}, @@ -753,10 +751,10 @@ def n(self) -> int: bch.k """, ) - @property def k(self) -> int: return super().k + @property @extend_docstring( _CyclicCode.d, {}, @@ -780,10 +778,10 @@ def k(self) -> int: bch.d """, ) - @property def d(self) -> int: return super().d + @property @extend_docstring( _CyclicCode.t, {}, @@ -804,10 +802,10 @@ def d(self) -> int: bch.t """, ) - @property def t(self) -> int: return super().t + @property @extend_docstring( _CyclicCode.generator_poly, {}, @@ -837,10 +835,10 @@ def t(self) -> int: bch.generator_poly(bch.roots, field=bch.extension_field) """, ) - @property def generator_poly(self) -> Poly: return super().generator_poly + @property @extend_docstring( _CyclicCode.parity_check_poly, {}, @@ -863,10 +861,10 @@ def generator_poly(self) -> Poly: bch.H """, ) - @property def parity_check_poly(self) -> Poly: return super().parity_check_poly + @property @extend_docstring( _CyclicCode.roots, {}, @@ -898,7 +896,6 @@ def parity_check_poly(self) -> Poly: bch.generator_poly(bch.roots, field=bch.extension_field) """, ) - @property def roots(self) -> FieldArray: return super().roots @@ -969,6 +966,7 @@ def c(self) -> int: """ return self._c + @property @extend_docstring( _CyclicCode.G, {}, @@ -995,10 +993,10 @@ def c(self) -> int: bch.generator_poly """, ) - @property def G(self) -> FieldArray: return super().G + @property @extend_docstring( _CyclicCode.H, {}, @@ -1021,7 +1019,6 @@ def G(self) -> FieldArray: bch.parity_check_poly """, ) - @property def H(self) -> FieldArray: return super().H @@ -1080,6 +1077,7 @@ def is_narrow_sense(self) -> bool: """ return self._is_narrow_sense + @property @extend_docstring( _CyclicCode.is_systematic, {}, @@ -1103,7 +1101,6 @@ def is_narrow_sense(self) -> bool: bch.generator_poly """, ) - @property def is_systematic(self) -> bool: return super().is_systematic @@ -1183,139 +1180,3 @@ def _generator_poly_from_k( break return best_generator_poly, best_roots - - -class bch_decode_jit(Function): - """ - Performs general BCH and Reed-Solomon decoding. - - References: - - Lin, S. and Costello, D. Error Control Coding. Section 7.4. - """ - - def __init__(self, field: Type[FieldArray], extension_field: Type[FieldArray]): - super().__init__(field) - self.extension_field = extension_field - - @property - def key_1(self): - # Make the key in the cache lookup table specific to both the base field and extension field - return ( - self.field.characteristic, - self.field.degree, - int(self.field.irreducible_poly), - self.extension_field.characteristic, - self.extension_field.degree, - int(self.extension_field.irreducible_poly), - ) - - def __call__(self, codeword, design_n, alpha, c, roots): - if self.extension_field.ufunc_mode != "python-calculate": - output = self.jit(codeword.astype(np.int64), design_n, alpha, c, roots.astype(np.int64)) - else: - output = self.python(codeword.view(np.ndarray), design_n, alpha, c, roots.view(np.ndarray)) - - dec_codeword, N_errors = output[:, 0:-1], output[:, -1] - dec_codeword = dec_codeword.astype(codeword.dtype) - dec_codeword = dec_codeword.view(self.field) - - return dec_codeword, N_errors - - def set_globals(self): - global CHARACTERISTIC, SUBTRACT, MULTIPLY, RECIPROCAL, POWER - global CONVOLVE, POLY_ROOTS, POLY_EVALUATE, BERLEKAMP_MASSEY - - SUBTRACT = self.field._subtract.ufunc_call_only - - CHARACTERISTIC = self.extension_field.characteristic - MULTIPLY = self.extension_field._multiply.ufunc_call_only - RECIPROCAL = self.extension_field._reciprocal.ufunc_call_only - POWER = self.extension_field._power.ufunc_call_only - CONVOLVE = self.extension_field._convolve.function - POLY_ROOTS = roots_jit(self.extension_field).function - POLY_EVALUATE = evaluate_elementwise_jit(self.extension_field).function - BERLEKAMP_MASSEY = berlekamp_massey_jit(self.extension_field).function - - _SIGNATURE = numba.types.FunctionType(int64[:, :](int64[:, :], int64, int64, int64, int64[:])) - - @staticmethod - def implementation(codewords, design_n, alpha, c, roots): # pragma: no cover - dtype = codewords.dtype - N = codewords.shape[0] # The number of codewords - n = codewords.shape[1] # The codeword size (could be less than the design n for shortened codes) - d = roots.size + 1 - t = (d - 1) // 2 - - # The last column of the returned decoded codeword is the number of corrected errors - dec_codewords = np.zeros((N, n + 1), dtype=dtype) - dec_codewords[:, 0:n] = codewords[:, :] - - for i in range(N): - # Compute the syndrome by evaluating each codeword at the roots of the generator polynomial. - # The syndrome vector is S = [S0, S1, ..., S2t-1] - syndrome = POLY_EVALUATE(codewords[i, :], roots) - - if np.all(syndrome == 0): - continue - - # The error pattern is defined as the polynomial e(x) = e_j1*x^j1 + e_j2*x^j2 + ... for j1 to jv, - # implying there are v errors. And δi = e_ji is the i-th error value and βi = α^ji is the i-th - # error-locator value and ji is the error location. - - # The error-locator polynomial σ(x) = (1 - β1*x)(1 - β2*x)...(1 - βv*x) where βi are the inverse of the - # roots of σ(x). - - # Compute the error-locator polynomial σ(x) - # TODO: Re-evaluate these equations since changing BMA to return the characteristic polynomial, - # not the feedback polynomial - sigma = BERLEKAMP_MASSEY(syndrome)[::-1] - v = sigma.size - 1 # The number of errors, which is the degree of the error-locator polynomial - - if v > t: - dec_codewords[i, -1] = -1 - continue - - # Compute βi^-1, the roots of σ(x) - degrees = np.arange(sigma.size - 1, -1, -1) - results = POLY_ROOTS(degrees, sigma, alpha) - beta_inv = results[0, :] # The roots βi^-1 of σ(x) - error_locations_inv = results[1, :] # The roots βi^-1 as powers of the primitive element α - error_locations = -error_locations_inv % design_n # The error locations as degrees of c(x) - - if np.any(error_locations > n - 1): - # Indicates there are "errors" in the zero-ed portion of a shortened code, which indicates there are - # actually more errors than alleged. Return failure to decode. - dec_codewords[i, -1] = -1 - continue - - if beta_inv.size != v: - dec_codewords[i, -1] = -1 - continue - - # Compute σ'(x) - sigma_prime = np.zeros(v, dtype=dtype) - for j in range(v): - degree = v - j - sigma_prime[j] = MULTIPLY(degree % CHARACTERISTIC, sigma[j]) # Scalar multiplication - - # The error-value evaluator polynomial Z0(x) = S0*σ0 + (S1*σ0 + S0*σ1)*x + (S2*σ0 + S1*σ1 + S0*σ2)*x^2 + ... - # with degree v-1 - Z0 = CONVOLVE(sigma[-v:], syndrome[0:v][::-1])[-v:] - - # The error value δi = -1 * βi^(1-c) * Z0(βi^-1) / σ'(βi^-1) - for j in range(v): - beta_i = POWER(beta_inv[j], c - 1) - # NOTE: poly_eval() expects a 1-D array of values - Z0_i = POLY_EVALUATE(Z0, np.array([beta_inv[j]], dtype=dtype))[0] - # NOTE: poly_eval() expects a 1-D array of values - sigma_prime_i = POLY_EVALUATE(sigma_prime, np.array([beta_inv[j]], dtype=dtype))[0] - delta_i = MULTIPLY(beta_i, Z0_i) - delta_i = MULTIPLY(delta_i, RECIPROCAL(sigma_prime_i)) - delta_i = SUBTRACT(0, delta_i) - dec_codewords[i, n - 1 - error_locations[j]] = SUBTRACT( - dec_codewords[i, n - 1 - error_locations[j]], delta_i - ) - - dec_codewords[i, -1] = v # The number of corrected errors - - return dec_codewords diff --git a/src/galois/_codes/_bm_decoder.py b/src/galois/_codes/_bm_decoder.py new file mode 100644 index 000000000..5bad8b68a --- /dev/null +++ b/src/galois/_codes/_bm_decoder.py @@ -0,0 +1,167 @@ +""" +A module containing a general Berlekamp-Massey decoder for BCH and Reed-Solomon codes. +""" +from __future__ import annotations + +from typing import Callable, Hashable, Type + +import numba +import numpy as np +import numpy.typing as npt +from numba import int64 + +from .._domains._function import Function +from .._fields import FieldArray +from .._lfsr import berlekamp_massey_jit +from .._polys._dense import evaluate_elementwise_jit, roots_jit + +CHARACTERISTIC: int +SUBTRACT: Callable[[int, int], int] +MULTIPLY: Callable[[int, int], int] +RECIPROCAL: Callable[[int], int] +POWER: Callable[[int, int], int] +CONVOLVE: Callable[[npt.NDArray, npt.NDArray], npt.NDArray] +POLY_ROOTS: Callable[[npt.NDArray, npt.NDArray, int], npt.NDArray] +POLY_EVALUATE: Callable[[npt.NDArray, npt.NDArray], npt.NDArray] +BERLEKAMP_MASSEY: Callable[[npt.NDArray], npt.NDArray] + + +class berlekamp_decode_jit(Function): + """ + Performs general BCH and Reed-Solomon decoding. + + References: + - Lin, S. and Costello, D. Error Control Coding. Section 7.4. + """ + + def __init__(self, field: Type[FieldArray], extension_field: Type[FieldArray]): + super().__init__(field) + self.extension_field = extension_field + + @property + def key_1(self) -> Hashable: + # Make the key in the cache lookup table specific to both the base field and extension field + return ( + self.field.characteristic, + self.field.degree, + int(self.field.irreducible_poly), + self.extension_field.characteristic, + self.extension_field.degree, + int(self.extension_field.irreducible_poly), + ) + + def __call__( + self, codeword: FieldArray, design_n: int, alpha: int, c: int, roots: FieldArray + ) -> tuple[FieldArray, npt.NDArray]: + if self.extension_field.ufunc_mode != "python-calculate": + output = self.jit(codeword.astype(np.int64), design_n, alpha, c, roots.astype(np.int64)) + else: + output = self.python(codeword.view(np.ndarray), design_n, alpha, c, roots.view(np.ndarray)) + + dec_codeword, N_errors = output[:, 0:-1], output[:, -1] + dec_codeword = dec_codeword.astype(codeword.dtype) + dec_codeword = dec_codeword.view(self.field) + + return dec_codeword, N_errors + + def set_globals(self) -> None: + global CHARACTERISTIC, SUBTRACT, MULTIPLY, RECIPROCAL, POWER + global CONVOLVE, POLY_ROOTS, POLY_EVALUATE, BERLEKAMP_MASSEY + + SUBTRACT = self.field._subtract.ufunc_call_only + + CHARACTERISTIC = self.extension_field.characteristic + MULTIPLY = self.extension_field._multiply.ufunc_call_only + RECIPROCAL = self.extension_field._reciprocal.ufunc_call_only + POWER = self.extension_field._power.ufunc_call_only + CONVOLVE = self.extension_field._convolve.function + POLY_ROOTS = roots_jit(self.extension_field).function + POLY_EVALUATE = evaluate_elementwise_jit(self.extension_field).function + BERLEKAMP_MASSEY = berlekamp_massey_jit(self.extension_field).function + + _SIGNATURE = numba.types.FunctionType(int64[:, :](int64[:, :], int64, int64, int64, int64[:])) + + @staticmethod + def implementation( + codewords: npt.NDArray, design_n: int, alpha: int, c: int, roots: npt.NDArray + ) -> npt.NDArray: # pragma: no cover + dtype = codewords.dtype + N = codewords.shape[0] # The number of codewords + n = codewords.shape[1] # The codeword size (could be less than the design n for shortened codes) + d = roots.size + 1 + t = (d - 1) // 2 # Number of correctable errors + + # The last column of the returned decoded codeword is the number of corrected errors + dec_codewords = np.zeros((N, n + 1), dtype=dtype) + dec_codewords[:, 0:n] = codewords[:, :] + + for i in range(N): + # Compute the syndrome by evaluating each codeword at the roots of the generator polynomial. + # The syndrome vector is S = [S_1, S_2, ..., S_2t] + syndrome = POLY_EVALUATE(codewords[i, :], roots) + + # If the syndrome is zero, then the codeword is a valid codeword and no errors need to be corrected. + if np.all(syndrome == 0): + continue + + # The error pattern is defined as the polynomial e(x) = e_j1*x^j1 + e_j2*x^j2 + ... for j1 to jν, + # implying there are ν errors. δi = e_ji is the i-th error value, βi = α^ji is the i-th + # error-location number, and ji is the error location. + + # The error-location polynomial σ(x) = (1 - β1*x)(1 - β2*x)...(1 - βν*x) where βi are the inverse of the + # roots of σ(x). + + # Compute the error-location polynomial σ(x) + # TODO: Re-evaluate these equations since changing BMA to return the characteristic polynomial, + # not the feedback polynomial + sigma = BERLEKAMP_MASSEY(syndrome)[::-1] + v = sigma.size - 1 # The number of errors, which is the degree of the error-locator polynomial + + if v > t: + dec_codewords[i, -1] = -1 + continue + + # Compute βi^-1, the roots of σ(x) + degrees = np.arange(sigma.size - 1, -1, -1) + results = POLY_ROOTS(degrees, sigma, alpha) + beta_inv = results[0, :] # The roots βi^-1 of σ(x) + error_locations_inv = results[1, :] # The roots βi^-1 as powers of the primitive element α + error_locations = -error_locations_inv % design_n # The error locations as degrees of c(x) + + if np.any(error_locations > n - 1): + # Indicates there are "errors" in the zero-ed portion of a shortened code, which indicates there are + # actually more errors than alleged. Return failure to decode. + dec_codewords[i, -1] = -1 + continue + + if beta_inv.size != v: + dec_codewords[i, -1] = -1 + continue + + # Compute σ'(x) + sigma_prime = np.zeros(v, dtype=dtype) + for j in range(v): + degree = v - j + sigma_prime[j] = MULTIPLY(degree % CHARACTERISTIC, sigma[j]) # Scalar multiplication + + # The error-value evaluator polynomial Z0(x) = S0*σ0 + (S1*σ0 + S0*σ1)*x + (S2*σ0 + S1*σ1 + S0*σ2)*x^2 + ... + # with degree v-1 + Z0 = CONVOLVE(sigma[-v:], syndrome[0:v][::-1])[-v:] + + # The error value δi = -1 * βi^(1-c) * Z0(βi^-1) / σ'(βi^-1) + for j in range(v): + beta_i = POWER(beta_inv[j], c - 1) + # NOTE: poly_eval() expects a 1-D array of values + Z0_i = POLY_EVALUATE(Z0, np.array([beta_inv[j]], dtype=dtype))[0] + # NOTE: poly_eval() expects a 1-D array of values + sigma_prime_i = POLY_EVALUATE(sigma_prime, np.array([beta_inv[j]], dtype=dtype))[0] + delta_i = MULTIPLY(beta_i, Z0_i) + delta_i = MULTIPLY(delta_i, RECIPROCAL(sigma_prime_i)) + delta_i = SUBTRACT(0, delta_i) + dec_codewords[i, n - 1 - error_locations[j]] = SUBTRACT( + dec_codewords[i, n - 1 - error_locations[j]], delta_i + ) + + dec_codewords[i, -1] = v # The number of corrected errors + + return dec_codewords diff --git a/src/galois/_codes/_cyclic.py b/src/galois/_codes/_cyclic.py index 77448dffb..5563fee66 100644 --- a/src/galois/_codes/_cyclic.py +++ b/src/galois/_codes/_cyclic.py @@ -4,9 +4,10 @@ from __future__ import annotations -from typing import overload +from typing import Any, cast, overload import numpy as np +import numpy.typing as npt from typing_extensions import Literal from .._fields import FieldArray @@ -97,7 +98,7 @@ def decode( codeword: ArrayLike, output: Literal["message", "codeword"] = "message", errors: Literal[True] = True, - ) -> tuple[FieldArray, int | np.ndarray]: ... + ) -> tuple[FieldArray, int | npt.NDArray]: ... @extend_docstring( _LinearCode.decode, @@ -120,7 +121,7 @@ def decode( $$c(x) = c_{n-1} x^{n-1} + \dots + c_1 x + c_0 \in \mathrm{GF}(q)[x]$$ """, ) - def decode(self, codeword, output="message", errors=False): + def decode(self, codeword: Any, output: Any = "message", errors: Any = False) -> Any: return super().decode(codeword, output=output, errors=errors) def _convert_codeword_to_message(self, codeword: FieldArray) -> FieldArray: @@ -131,6 +132,7 @@ def _convert_codeword_to_message(self, codeword: FieldArray) -> FieldArray: message = codeword[..., 0:ks] else: message, _ = divmod_jit(self.field)(codeword, self.generator_poly.coeffs) + message = cast(FieldArray, message) return message @@ -139,6 +141,7 @@ def _convert_codeword_to_parity(self, codeword: FieldArray) -> FieldArray: parity = codeword[..., -(self.n - self.k) :] else: _, parity = divmod_jit(self.field)(codeword, self.generator_poly.coeffs) + parity = cast(FieldArray, parity) return parity @@ -216,6 +219,7 @@ def _poly_to_generator_matrix(n: int, generator_poly: Poly, systematic: bool) -> G = GF.Zeros((k, n)) for i in range(k): G[i, i : i + generator_poly.degree + 1] = generator_poly.coeffs + G = cast(FieldArray, G) return G diff --git a/src/galois/_codes/_linear.py b/src/galois/_codes/_linear.py index f5d9889b7..4f514c8a9 100644 --- a/src/galois/_codes/_linear.py +++ b/src/galois/_codes/_linear.py @@ -4,9 +4,10 @@ from __future__ import annotations -from typing import Type, overload +from typing import Any, Type, cast, overload import numpy as np +import numpy.typing as npt from typing_extensions import Literal from .._fields import FieldArray @@ -91,7 +92,7 @@ def encode(self, message: ArrayLike, output: Literal["codeword", "parity"] = "co parity = self._convert_codeword_to_parity(codeword) return parity - def detect(self, codeword: ArrayLike) -> bool | np.ndarray: + def detect(self, codeword: ArrayLike) -> bool | npt.NDArray: r""" Detects if errors are present in the codeword $\mathbf{c}$. @@ -112,7 +113,7 @@ def detect(self, codeword: ArrayLike) -> bool | np.ndarray: detected = self._detect_errors(codeword) if is_codeword_1d: - detected = bool(detected[0]) + return bool(detected[0]) return detected @@ -130,9 +131,9 @@ def decode( codeword: ArrayLike, output: Literal["message", "codeword"] = "message", errors: Literal[True] = True, - ) -> tuple[FieldArray, int | np.ndarray]: ... + ) -> tuple[FieldArray, int | npt.NDArray]: ... - def decode(self, codeword, output="message", errors=False): + def decode(self, codeword: Any, output: Any = "message", errors: Any = False) -> Any: r""" Decodes the codeword $\mathbf{c}$ into the message $\mathbf{m}$. @@ -214,11 +215,11 @@ def _check_and_convert_message(self, message: ArrayLike) -> tuple[FieldArray, bo # Record if the original message was 1-D and then convert to 2-D is_message_1d = message.ndim == 1 - message = np.atleast_2d(message) + message = cast(FieldArray, np.atleast_2d(message)) return message, is_message_1d - def _check_and_convert_codeword(self, codeword: FieldArray) -> FieldArray: + def _check_and_convert_codeword(self, codeword: ArrayLike) -> tuple[FieldArray, bool]: """ Converts the array-like codeword into a 2-D FieldArray with shape (N, ns). """ @@ -240,7 +241,7 @@ def _check_and_convert_codeword(self, codeword: FieldArray) -> FieldArray: # Record if the original codeword was 1-D and then convert to 2-D is_codeword_1d = codeword.ndim == 1 - codeword = np.atleast_2d(codeword) + codeword = cast(FieldArray, np.atleast_2d(codeword)) return codeword, is_codeword_1d @@ -269,13 +270,13 @@ def _encode_message(self, message: FieldArray) -> FieldArray: if self.is_systematic: parity = message @ self.G[-ks:, self.k :] - codeword = np.hstack((message, parity)) + codeword = cast(FieldArray, np.hstack((message, parity))) else: codeword = message @ self.G return codeword - def _detect_errors(self, codeword: FieldArray) -> np.ndarray: + def _detect_errors(self, codeword: FieldArray) -> npt.NDArray: """ Returns a boolean array (N,) indicating if errors are present in the codeword. """ @@ -289,7 +290,7 @@ def _detect_errors(self, codeword: FieldArray) -> np.ndarray: return detected - def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, np.ndarray]: + def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, npt.NDArray]: """ Decodes errors in the received codeword. Returns the corrected codeword (N, ns) and array of number of corrected errors (N,). @@ -383,11 +384,11 @@ def generator_to_parity_check_matrix(G: FieldArray) -> FieldArray: Arguments: G: The $(k, n)$ generator matrix $\mathbf{G}$ in systematic form - $\mathbf{G} = [\mathbf{I}_{k,k}\ |\ \mathbf{P}_{k,n-k}]$. + $\mathbf{G} = [\mathbf{I}_{k,k} \mid \mathbf{P}_{k,n-k}]$. Returns: The $(n-k, n)$ parity-check matrix - $\mathbf{H} = [-\mathbf{P}_{k,n-k}^T\ |\ \mathbf{I}_{n-k,n-k}]$`. + $\mathbf{H} = [-\mathbf{P}_{k,n-k}^T \mid \mathbf{I}_{n-k,n-k}]$`. Examples: .. ipython:: python @@ -409,7 +410,7 @@ def generator_to_parity_check_matrix(G: FieldArray) -> FieldArray: P = G[:, k:] I = field.Identity(n - k) - H = np.hstack((-P.T, I)) + H = cast(FieldArray, np.hstack((-P.T, I))) return H @@ -423,10 +424,10 @@ def parity_check_to_generator_matrix(H: FieldArray) -> FieldArray: Arguments: H: The $(n-k, n)$ parity-check matrix $\mathbf{G}$ in systematic form - $\mathbf{H} = [-\mathbf{P}_{k,n-k}^T\ |\ \mathbf{I}_{n-k,n-k}]$`. + $\mathbf{H} = [-\mathbf{P}_{k,n-k}^T \mid \mathbf{I}_{n-k,n-k}]$`. Returns: - The $(k, n)$ generator matrix $\mathbf{G} = [\mathbf{I}_{k,k}\ |\ \mathbf{P}_{k,n-k}]$. + The $(k, n)$ generator matrix $\mathbf{G} = [\mathbf{I}_{k,k} \mid \mathbf{P}_{k,n-k}]$. Examples: .. ipython:: python @@ -450,6 +451,6 @@ def parity_check_to_generator_matrix(H: FieldArray) -> FieldArray: P = -H[:, 0:k].T I = field.Identity(k) - G = np.hstack((I, P)) + G = cast(FieldArray, np.hstack((I, P))) return G diff --git a/src/galois/_codes/_reed_solomon.py b/src/galois/_codes/_reed_solomon.py index e80d0f90a..effdfaa9e 100644 --- a/src/galois/_codes/_reed_solomon.py +++ b/src/galois/_codes/_reed_solomon.py @@ -4,9 +4,10 @@ from __future__ import annotations -from typing import Type, overload +from typing import Any, Type, cast, overload import numpy as np +import numpy.typing as npt from typing_extensions import Literal from .._fields import Field, FieldArray @@ -14,7 +15,7 @@ from .._math import ilog from .._polys import Poly, matlab_primitive_poly from ..typing import ArrayLike, ElementLike -from ._bch import bch_decode_jit +from ._bm_decoder import berlekamp_decode_jit from ._cyclic import _CyclicCode @@ -182,7 +183,7 @@ def __init__( super().__init__(n, k, d, generator_poly, roots, systematic) # TODO: Do this?? How to standardize G and H? - self._H = np.power.outer(roots, np.arange(n - 1, -1, -1, dtype=field.dtypes[-1])) + self._H = cast(FieldArray, np.power.outer(roots, np.arange(n - 1, -1, -1, dtype=field.dtypes[-1]))) def __repr__(self) -> str: r""" @@ -429,7 +430,7 @@ def encode(self, message: ArrayLike, output: Literal["codeword", "parity"] = "co rs.detect(c) """, ) - def detect(self, codeword: ArrayLike) -> bool | np.ndarray: + def detect(self, codeword: ArrayLike) -> bool | npt.NDArray: return super().detect(codeword) @overload @@ -446,7 +447,7 @@ def decode( codeword: ArrayLike, output: Literal["message", "codeword"] = "message", errors: Literal[True] = True, - ) -> tuple[FieldArray, int | np.ndarray]: ... + ) -> tuple[FieldArray, int | npt.NDArray]: ... @extend_docstring( _CyclicCode.decode, @@ -602,11 +603,11 @@ def decode( np.array_equal(d, m) """, ) - def decode(self, codeword, output="message", errors=False): + def decode(self, codeword: Any, output: Any = "message", errors: Any = False) -> Any: return super().decode(codeword, output=output, errors=errors) - def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, np.ndarray]: - func = reed_solomon_decode_jit(self.field, self.field) + def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, npt.NDArray]: + func = berlekamp_decode_jit(self.field, self.field) dec_codeword, N_errors = func(codeword, self.n, int(self.alpha), self.c, self.roots) dec_codeword = dec_codeword.view(self.field) return dec_codeword, N_errors @@ -637,6 +638,7 @@ def _decode_codeword(self, codeword: FieldArray) -> tuple[FieldArray, np.ndarray def field(self) -> Type[FieldArray]: return super().field + @property @extend_docstring( _CyclicCode.n, {}, @@ -657,10 +659,10 @@ def field(self) -> Type[FieldArray]: rs.n """, ) - @property def n(self) -> int: return super().n + @property @extend_docstring( _CyclicCode.k, {}, @@ -681,10 +683,10 @@ def n(self) -> int: rs.k """, ) - @property def k(self) -> int: return super().k + @property @extend_docstring( _CyclicCode.d, {}, @@ -705,10 +707,10 @@ def k(self) -> int: rs.d """, ) - @property def d(self) -> int: return super().d + @property @extend_docstring( _CyclicCode.t, {}, @@ -729,10 +731,10 @@ def d(self) -> int: rs.t """, ) - @property def t(self) -> int: return super().t + @property @extend_docstring( _CyclicCode.generator_poly, {}, @@ -761,10 +763,10 @@ def t(self) -> int: rs.generator_poly(rs.roots) """, ) - @property def generator_poly(self) -> Poly: return super().generator_poly + @property @extend_docstring( _CyclicCode.parity_check_poly, {}, @@ -787,10 +789,10 @@ def generator_poly(self) -> Poly: rs.H """, ) - @property def parity_check_poly(self) -> Poly: return super().parity_check_poly + @property @extend_docstring( _CyclicCode.roots, {}, @@ -821,7 +823,6 @@ def parity_check_poly(self) -> Poly: rs.generator_poly(rs.roots) """, ) - @property def roots(self) -> FieldArray: return super().roots @@ -888,6 +889,7 @@ def c(self) -> int: """ return self._c + @property @extend_docstring( _CyclicCode.G, {}, @@ -914,10 +916,10 @@ def c(self) -> int: rs.generator_poly """, ) - @property def G(self) -> FieldArray: return super().G + @property @extend_docstring( _CyclicCode.H, {}, @@ -940,7 +942,6 @@ def G(self) -> FieldArray: rs.parity_check_poly """, ) - @property def H(self) -> FieldArray: return super().H @@ -1000,6 +1001,7 @@ def is_narrow_sense(self) -> bool: """ return self._is_narrow_sense + @property @extend_docstring( _CyclicCode.is_systematic, {}, @@ -1023,17 +1025,5 @@ def is_narrow_sense(self) -> bool: rs.generator_poly """, ) - @property def is_systematic(self) -> bool: return super().is_systematic - - -class reed_solomon_decode_jit(bch_decode_jit): - """ - Performs general BCH and Reed-Solomon decoding. - - References: - - Lin, S. and Costello, D. Error Control Coding. Section 7.4. - """ - - # NOTE: Making a subclass so that these compiled functions are stored in a new namespace diff --git a/src/galois/_databases/_interface.py b/src/galois/_databases/_interface.py index afaf270bd..500d101c9 100644 --- a/src/galois/_databases/_interface.py +++ b/src/galois/_databases/_interface.py @@ -7,6 +7,7 @@ import sqlite3 import sys from pathlib import Path +from threading import Lock class DatabaseInterface: @@ -14,15 +15,17 @@ class DatabaseInterface: An abstract class to interface with SQLite databases. """ - singleton = None + _lock = Lock() + _singleton = None file: Path def __new__(cls): - if cls.singleton is None: - cls.singleton = super().__new__(cls) - cls.conn = sqlite3.connect(cls.file) - cls.cursor = cls.conn.cursor() - return cls.singleton + with cls._lock: + if cls._singleton is None: + cls._singleton = super().__new__(cls) + cls.conn = sqlite3.connect(cls.file, check_same_thread=False) + cls.cursor = cls.conn.cursor() + return cls._singleton class PrimeFactorsDatabase(DatabaseInterface): @@ -30,7 +33,6 @@ class PrimeFactorsDatabase(DatabaseInterface): A class to interface with the prime factors database. """ - singleton = None file = Path(__file__).parent / "prime_factors.db" def fetch(self, n: int) -> tuple[list[int], list[int], int]: @@ -48,18 +50,21 @@ def fetch(self, n: int) -> tuple[list[int], list[int], int]: if hasattr(sys, "set_int_max_str_digits"): default_limit = sys.get_int_max_str_digits() sys.set_int_max_str_digits(0) - n = str(n) + n_str = str(n) sys.set_int_max_str_digits(default_limit) - - self.cursor.execute( - """ - SELECT factors, multiplicities, composite - FROM factorizations - WHERE value=? - """, - (str(n),), - ) - result = self.cursor.fetchone() + else: + n_str = str(n) + + with self._lock: + self.cursor.execute( + """ + SELECT factors, multiplicities, composite + FROM factorizations + WHERE value=? + """, + (n_str,), + ) + result = self.cursor.fetchone() if result is None: raise LookupError(f"The prime factors database does not contain an entry for {n}.") @@ -76,7 +81,6 @@ class IrreduciblePolyDatabase(DatabaseInterface): A class to interface with the irreducible polynomials database. """ - singleton = None file = Path(__file__).parent / "irreducible_polys.db" def fetch(self, characteristic: int, degree: int) -> tuple[list[int], list[int]]: @@ -90,14 +94,15 @@ def fetch(self, characteristic: int, degree: int) -> tuple[list[int], list[int]] Returns: A tuple containing the non-zero degrees and coefficients of the irreducible polynomial. """ - self.cursor.execute( - """ - SELECT nonzero_degrees, nonzero_coeffs - FROM polys - WHERE characteristic=? AND degree=?""", - (characteristic, degree), - ) - result = self.cursor.fetchone() + with self._lock: + self.cursor.execute( + """ + SELECT nonzero_degrees, nonzero_coeffs + FROM polys + WHERE characteristic=? AND degree=?""", + (characteristic, degree), + ) + result = self.cursor.fetchone() if result is None: raise LookupError( @@ -116,7 +121,6 @@ class ConwayPolyDatabase(DatabaseInterface): A class to interface with the Conway polynomials database. """ - singleton = None file = Path(__file__).parent / "conway_polys.db" def fetch(self, characteristic: int, degree: int) -> tuple[list[int], list[int]]: @@ -130,15 +134,16 @@ def fetch(self, characteristic: int, degree: int) -> tuple[list[int], list[int]] Returns: A tuple containing the non-zero degrees and coefficients of the Conway polynomial. """ - self.cursor.execute( - """ - SELECT nonzero_degrees, nonzero_coeffs - FROM polys - WHERE characteristic=? AND degree=? - """, - (characteristic, degree), - ) - result = self.cursor.fetchone() + with self._lock: + self.cursor.execute( + """ + SELECT nonzero_degrees, nonzero_coeffs + FROM polys + WHERE characteristic=? AND degree=? + """, + (characteristic, degree), + ) + result = self.cursor.fetchone() if result is None: raise LookupError( diff --git a/src/galois/_domains/_array.py b/src/galois/_domains/_array.py index 283de9d49..97b9c2493 100644 --- a/src/galois/_domains/_array.py +++ b/src/galois/_domains/_array.py @@ -7,7 +7,7 @@ import abc import contextlib import random -from typing import Generator +from typing import Generator, cast, no_type_check import numpy as np import numpy.typing as npt @@ -86,7 +86,7 @@ def _verify_array_like_types_and_values(cls, x: ElementLike | ArrayLike) -> Elem @classmethod @abc.abstractmethod - def _verify_element_types_and_convert(cls, array: np.ndarray, object_=False) -> np.ndarray: + def _verify_element_types_and_convert(cls, array: npt.NDArray, object_=False) -> npt.NDArray: """ Iterate across each element and verify it's a valid type. Also, convert strings to integers along the way. """ @@ -100,7 +100,7 @@ def _verify_scalar_value(cls, scalar: int): @classmethod @abc.abstractmethod - def _verify_array_values(cls, array: np.ndarray): + def _verify_array_values(cls, array: npt.NDArray): """ Verify all the elements of the integer array are within the valid range [0, order). """ @@ -118,7 +118,7 @@ def _convert_to_element(cls, element: ElementLike) -> int: @classmethod @abc.abstractmethod - def _convert_iterable_to_elements(cls, iterable: IterableLike) -> np.ndarray: + def _convert_iterable_to_elements(cls, iterable: IterableLike) -> npt.NDArray: """ Convert an iterable (recursive) to a NumPy integer array. Convert any strings to integers along the way. """ @@ -128,7 +128,7 @@ def _convert_iterable_to_elements(cls, iterable: IterableLike) -> np.ndarray: ############################################################################### @classmethod - def _view(cls, array: np.ndarray) -> Self: + def _view(cls, array: npt.NDArray) -> Self: """ View the input array to the Array subclass `A` using the `_view_without_verification()` context manager. This disables bounds checking on the array elements. Instead of `x.view(A)` use `A._view(x)`. @@ -137,7 +137,7 @@ def _view(cls, array: np.ndarray) -> Self: """ with cls._view_without_verification(): array = array.view(cls) - return array + return cast(cls, array) @classmethod @contextlib.contextmanager @@ -272,7 +272,7 @@ def Random( if seed is not None: if not isinstance(seed, (int, np.integer, np.random.Generator)): raise ValueError("Seed must be an integer, a numpy.random.Generator or None.") - if isinstance(seed, (int, np.integer)) and seed < 0: + if isinstance(seed, int) and seed < 0: raise ValueError("Seed must be non-negative.") if dtype != np.object_: @@ -410,7 +410,7 @@ def _repr_context_manager(cls, element_repr: Literal["int", "poly", "power"]): # Override getters/setters and type conversion functions ############################################################################### - def __getitem__(self, key): + def __getitem__(self, key) -> Self: """ Ensure that slices that return a single value return a 0-D Galois field array and not a single integer. This ensures subsequent arithmetic with the finite field scalar works properly. @@ -454,92 +454,122 @@ def astype(self, dtype, order="K", casting="unsafe", subok=True, copy=True): # Override arithmetic operators so type checking is appeased ############################################################################### + @no_type_check def __add__(self, other: npt.NDArray) -> Self: return super().__add__(other) + @no_type_check def __iadd__(self, other: npt.NDArray) -> Self: return super().__iadd__(other) + @no_type_check def __radd__(self, other: npt.NDArray) -> Self: return super().__radd__(other) + @no_type_check def __sub__(self, other: npt.NDArray) -> Self: return super().__sub__(other) + @no_type_check def __isub__(self, other: npt.NDArray) -> Self: return super().__isub__(other) + @no_type_check def __rsub__(self, other: npt.NDArray) -> Self: return super().__rsub__(other) + @no_type_check def __mul__(self, other: int | npt.NDArray) -> Self: return super().__mul__(other) + @no_type_check def __imul__(self, other: int | npt.NDArray) -> Self: return super().__imul__(other) + @no_type_check def __rmul__(self, other: int | npt.NDArray) -> Self: return super().__rmul__(other) + @no_type_check def __truediv__(self, other: npt.NDArray) -> Self: return super().__truediv__(other) + @no_type_check def __itruediv__(self, other: npt.NDArray) -> Self: return super().__itruediv__(other) + @no_type_check def __rtruediv__(self, other: npt.NDArray) -> Self: return super().__rtruediv__(other) + @no_type_check def __floordiv__(self, other: npt.NDArray) -> Self: return super().__floordiv__(other) + @no_type_check def __ifloordiv__(self, other: npt.NDArray) -> Self: return super().__ifloordiv__(other) + @no_type_check def __rfloordiv__(self, other: npt.NDArray) -> Self: return super().__rfloordiv__(other) + @no_type_check def __neg__(self) -> Self: return super().__neg__() + @no_type_check def __mod__(self, other: npt.NDArray) -> Self: return super().__mod__(other) + @no_type_check def __imod__(self, other: npt.NDArray) -> Self: return super().__imod__(other) + @no_type_check def __rmod__(self, other: npt.NDArray) -> Self: return super().__rmod__(other) + @no_type_check def __pow__(self, other: int | npt.NDArray) -> Self: return super().__pow__(other) + @no_type_check def __ipow__(self, other: int | npt.NDArray) -> Self: return super().__ipow__(other) + # @no_type_check # def __rpow__(self, other) -> Self: # return super().__rpow__(other) + @no_type_check def __matmul__(self, other: npt.NDArray) -> Self: return super().__matmul__(other) + @no_type_check def __rmatmul__(self, other: npt.NDArray) -> Self: return super().__rmatmul__(other) + @no_type_check def __lshift__(self, other: int | npt.NDArray) -> Self: return super().__lshift__(other) + @no_type_check def __ilshift__(self, other: int | npt.NDArray) -> Self: return super().__ilshift__(other) + # @no_type_check # def __rlshift__(self, other) -> Self: # return super().__rlshift__(other) + @no_type_check def __rshift__(self, other: int | npt.NDArray) -> Self: return super().__rshift__(other) + @no_type_check def __irshift__(self, other: int | npt.NDArray) -> Self: return super().__irshift__(other) + # @no_type_check # def __rrshift__(self, other) -> Self: # return super().__rrshift__(other) diff --git a/src/galois/_domains/_calculate.py b/src/galois/_domains/_calculate.py index c93cd68cd..829685ea1 100644 --- a/src/galois/_domains/_calculate.py +++ b/src/galois/_domains/_calculate.py @@ -3,10 +3,11 @@ each type of arithmetic are implemented here. """ -from typing import Type +from typing import Callable, Type import numba import numpy as np +import numpy.typing as npt from .._prime import factors from . import _lookup @@ -16,11 +17,30 @@ # Helper JIT functions ############################################################################### +CHARACTERISTIC: int +DEGREE: int +ORDER: int +IRREDUCIBLE_POLY: int + +INT_TO_VECTOR: Callable[[int, int, int], npt.NDArray] +VECTOR_TO_INT: Callable[[npt.NDArray, int, int], int] +EGCD: Callable[[int, int], npt.NDArray] +CRT: Callable[[npt.NDArray, npt.NDArray], int] DTYPE = np.int64 +MULTIPLY: Callable[[int, int], int] +RECIPROCAL: Callable[[int], int] +SUBFIELD_RECIPROCAL: Callable[[int], int] +POWER: Callable[[int, int], int] +POSITIVE_POWER: Callable[[int, int], int] +BRUTE_FORCE_LOG: Callable[[int, int], int] + +FACTORS: npt.NDArray +MULTIPLICITIES: npt.NDArray + @numba.jit(["int64[:](int64, int64, int64)"], nopython=True, cache=True) -def int_to_vector(a: int, characteristic: int, degree: int) -> np.ndarray: +def int_to_vector(a: int, characteristic: int, degree: int) -> npt.NDArray: """ Converts the integer representation to vector/polynomial representation. """ @@ -34,7 +54,7 @@ def int_to_vector(a: int, characteristic: int, degree: int) -> np.ndarray: @numba.jit(["int64(int64[:], int64, int64)"], nopython=True, cache=True) -def vector_to_int(a_vec: np.ndarray, characteristic: int, degree: int) -> int: +def vector_to_int(a_vec: npt.NDArray, characteristic: int, degree: int) -> int: """ Converts the vector/polynomial representation to the integer representation. """ @@ -48,7 +68,7 @@ def vector_to_int(a_vec: np.ndarray, characteristic: int, degree: int) -> int: @numba.jit(["int64[:](int64, int64)"], nopython=True, cache=True) -def egcd(a: int, b: int) -> np.ndarray: # pragma: no cover +def egcd(a: int, b: int) -> npt.NDArray: # pragma: no cover """ Computes the Extended Euclidean Algorithm. Returns (d, s, t). @@ -73,12 +93,11 @@ def egcd(a: int, b: int) -> np.ndarray: # pragma: no cover return np.array([r2, s2, t2], dtype=DTYPE) - EGCD = egcd @numba.jit(["int64(int64[:], int64[:])"], nopython=True, cache=True) -def crt(remainders: np.ndarray, moduli: np.ndarray) -> int: # pragma: no cover +def crt(remainders: npt.NDArray, moduli: npt.NDArray) -> int: # pragma: no cover """ Computes the simultaneous solution to the system of congruences xi == ai (mod mi). """ diff --git a/src/galois/_domains/_function.py b/src/galois/_domains/_function.py index eaf7d4a9f..12934bcf2 100644 --- a/src/galois/_domains/_function.py +++ b/src/galois/_domains/_function.py @@ -5,7 +5,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Type +from abc import abstractmethod +from typing import TYPE_CHECKING, Callable, Type, cast import numba import numpy as np @@ -29,7 +30,8 @@ class Function: def __init__(self, field: Type[Array]): self.field = field - def __call__(self): + @abstractmethod + def __call__(self, *args, **kwargs): """ Invokes the function, either JIT-compiled or pure-Python, performing necessary input/output conversion. """ @@ -48,8 +50,12 @@ def set_globals(self): _PARALLEL = False """Indicates if parallel processing should be performed.""" - implementation: Callable - """The function's implementation in pure Python.""" + @staticmethod + def implementation(*args, **kwargs): + """ + The function's implementation in pure Python. + """ + pass ############################################################################### # Various ufuncs based on implementation and compilation @@ -104,6 +110,13 @@ def python(self) -> Callable: # Ndarray function wrappers ############################################################################### +CHARACTERISTIC: int +IS_PRIME_FIELD: bool + +ADD: Callable[[int, int], int] +SUBTRACT: Callable[[int, int], int] +MULTIPLY: Callable[[int, int], int] + class convolve_jit(Function): """ @@ -182,7 +195,7 @@ def __call__(self, x: Array, n=None, axis=-1, norm=None) -> Array: if n is None: n = x.size - x = np.append(x, np.zeros(n - x.size, dtype=x.dtype)) + x = cast(Array, np.append(x, np.zeros(n - x.size, dtype=x.dtype))) omega = self.field.primitive_root_of_unity(x.size) if self._direction == "backward": @@ -342,9 +355,9 @@ class FunctionMixin(np.ndarray, metaclass=ArrayMeta): def __init_subclass__(cls) -> None: super().__init_subclass__() - cls._convolve = convolve_jit(cls) - cls._fft = fft_jit(cls) - cls._ifft = ifft_jit(cls) + cls._convolve = convolve_jit(cls) # type: ignore + cls._fft = fft_jit(cls) # type: ignore + cls._ifft = ifft_jit(cls) # type: ignore def __array_function__(self, func, types, args, kwargs): """ diff --git a/src/galois/_domains/_linalg.py b/src/galois/_domains/_linalg.py index c7858fa37..68b29425c 100644 --- a/src/galois/_domains/_linalg.py +++ b/src/galois/_domains/_linalg.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Type, cast import numba import numpy as np @@ -32,31 +32,31 @@ def _lapack_linalg(field: Type[Array], a: Array, b: Array, function, out=None, n else: return_dtype = a.dtype if np.iinfo(a.dtype).max < np.iinfo(b.dtype).max else b.dtype - a = a.view(np.ndarray) - b = b.view(np.ndarray) + a_np = a.view(np.ndarray) + b_np = b.view(np.ndarray) # Determine the minimum dtype to hold the entire product and summation without overflowing if n_sum is None: - n_sum = 1 if len(a.shape) == 0 else max(a.shape) + n_sum = 1 if len(a_np.shape) == 0 else max(a_np.shape) max_value = n_sum * (field.characteristic - 1) ** 2 dtypes = [dtype for dtype in DTYPES if np.iinfo(dtype).max >= max_value] dtype = np.object_ if len(dtypes) == 0 else dtypes[0] - a = a.astype(dtype) - b = b.astype(dtype) + a_np = a_np.astype(dtype) + b_np = b_np.astype(dtype) # Compute result using native NumPy LAPACK/BLAS implementation if function in [np.inner, np.vdot]: # These functions don't have and `out` keyword argument - c = function(a, b) + cc = function(a_np, b_np) else: - c = function(a, b, out=out) - c = c % field.characteristic # Reduce the result mod p + cc = function(a_np, b_np, out=out) + cc = cc % field.characteristic # Reduce the result mod p - if np.isscalar(c): + if np.isscalar(cc): # TODO: Sometimes the scalar c is a float? - c = field(int(c), dtype=return_dtype) + c = field(int(cc), dtype=return_dtype) else: - c = field._view(c.astype(return_dtype)) + c = field._view(cc.astype(return_dtype)) return c @@ -114,8 +114,8 @@ def __call__(self, a: Array, b: Array) -> Array: if self.field._is_prime_field: return _lapack_linalg(self.field, a, b, np.vdot) - a = a.flatten() - b = b.flatten().reshape(a.shape) # This is done to mimic NumPy's error scenarios + a = cast(Array, a.flatten()) + b = cast(Array, b.flatten().reshape(a.shape)) # This is done to mimic NumPy's error scenarios return np.sum(a * b) @@ -160,7 +160,7 @@ def __call__(self, a: Array, b: Array, out=None) -> Array: if self.field._is_prime_field: return _lapack_linalg(self.field, a, b, np.outer, out=out, n_sum=1) - return np.multiply.outer(a.ravel(), b.ravel(), out=out) + return cast(Array, np.multiply.outer(a.ravel(), b.ravel(), out=out)) class matmul_jit(Function): @@ -192,10 +192,10 @@ def __call__(self, A: Array, B: Array, out=None, **kwargs) -> Array: prepend, append = False, False if A.ndim == 1: - A = A.reshape((1, A.size)) + A = cast(Array, A.reshape((1, A.size))) prepend = True if B.ndim == 1: - B = B.reshape((B.size, 1)) + B = cast(Array, B.reshape((B.size, 1))) append = True if not A.shape[-1] == B.shape[-2]: @@ -533,11 +533,11 @@ class LinalgFunctionMixin(FunctionMixin): def __init_subclass__(cls) -> None: super().__init_subclass__() - cls._dot = dot_jit(cls) - cls._vdot = vdot_jit(cls) - cls._inner = inner_jit(cls) - cls._outer = outer_jit(cls) - cls._det = det_jit(cls) - cls._matrix_rank = matrix_rank_jit(cls) - cls._solve = solve_jit(cls) - cls._inv = inv_jit(cls) + cls._dot = dot_jit(cls) # type: ignore + cls._vdot = vdot_jit(cls) # type: ignore + cls._inner = inner_jit(cls) # type: ignore + cls._outer = outer_jit(cls) # type: ignore + cls._det = det_jit(cls) # type: ignore + cls._matrix_rank = matrix_rank_jit(cls) # type: ignore + cls._solve = solve_jit(cls) # type: ignore + cls._inv = inv_jit(cls) # type: ignore diff --git a/src/galois/_domains/_lookup.py b/src/galois/_domains/_lookup.py index ade5fdfe8..da8206ed0 100644 --- a/src/galois/_domains/_lookup.py +++ b/src/galois/_domains/_lookup.py @@ -9,12 +9,20 @@ from typing import TYPE_CHECKING import numpy as np +import numpy.typing as npt from . import _ufunc if TYPE_CHECKING: from ._array import Array +ORDER: int + +LOG: npt.NDArray +EXP: npt.NDArray +ZECH_LOG: npt.NDArray +ZECH_E: int + class add_ufunc(_ufunc.add_ufunc): """ diff --git a/src/galois/_fields/_array.py b/src/galois/_fields/_array.py index 2895a4563..d32d89973 100644 --- a/src/galois/_fields/_array.py +++ b/src/galois/_fields/_array.py @@ -4,9 +4,10 @@ from __future__ import annotations -from typing import Generator +from typing import Generator, cast import numpy as np +import numpy.typing as npt from typing_extensions import Literal, Self from .._domains import Array, _linalg @@ -128,7 +129,7 @@ def _verify_array_like_types_and_values(cls, x: ElementLike | ArrayLike) -> Elem if isinstance(x, (int, np.integer)): cls._verify_scalar_value(x) elif isinstance(x, cls): - # This was a previously-created and vetted array -- there's no need to re-verify + # This was a previously created and vetted array -- there's no need to re-verify if x.ndim == 0: # Ensure that in "large" fields with dtype=object that FieldArray objects aren't assigned to the array. # The arithmetic functions are designed to operate on Python ints. @@ -155,7 +156,7 @@ def _verify_array_like_types_and_values(cls, x: ElementLike | ArrayLike) -> Elem return x @classmethod - def _verify_element_types_and_convert(cls, array: np.ndarray, object_=False) -> np.ndarray: + def _verify_element_types_and_convert(cls, array: npt.NDArray, object_=False) -> npt.NDArray: if array.size == 0: return array if object_: @@ -168,7 +169,7 @@ def _verify_scalar_value(cls, scalar: int): raise ValueError(f"{cls.name} scalars must be in `0 <= x < {cls.order}`, not {scalar}.") @classmethod - def _verify_array_values(cls, array: np.ndarray): + def _verify_array_values(cls, array: npt.NDArray): if np.any(array < 0) or np.any(array >= cls.order): idxs = np.logical_or(array < 0, array >= cls.order) values = array if array.ndim == 0 else array[idxs] @@ -192,7 +193,7 @@ def _convert_to_element(cls, element: ElementLike) -> int: return element @classmethod - def _convert_iterable_to_elements(cls, iterable: IterableLike) -> np.ndarray: + def _convert_iterable_to_elements(cls, iterable: IterableLike) -> npt.NDArray: if cls.dtypes == [np.object_]: array = np.array(iterable, dtype=object) array = cls._verify_element_types_and_convert(array, object_=True) @@ -369,7 +370,7 @@ def Vandermonde(cls, element: ElementLike, rows: int, cols: int, dtype: DTypeLik raise ValueError(f"Argument 'element' must be element scalar, not {element.ndim}-D.") v = element ** np.arange(0, rows) - V = np.power.outer(v, np.arange(0, cols)) + V = cast(cls, np.power.outer(v, np.arange(0, cols))) return V @@ -415,14 +416,15 @@ def Vector(cls, array: ArrayLike, dtype: DTypeLike | None = None) -> FieldArray: degree = cls.degree x = cls.prime_subfield(array) # Convert element-like objects into the prime subfield - x = x.view(np.ndarray) # Convert into an integer array - if not x.shape[-1] == degree: + x_np = x.view(np.ndarray) # Convert into an integer array + if not x_np.shape[-1] == degree: raise ValueError( - f"The last dimension of `array` must be the field extension dimension {cls.degree}, not {x.shape[-1]}." + f"The last dimension of `array` must be the field extension dimension {cls.degree}, " + f"not {x_np.shape[-1]}." ) degrees = np.arange(degree - 1, -1, -1, dtype=dtype) - y = np.sum(x * order**degrees, axis=-1, dtype=dtype) + y = np.sum(x_np * order**degrees, axis=-1, dtype=dtype) if np.isscalar(y): y = cls(y, dtype=dtype) @@ -960,7 +962,7 @@ def primitive_roots_of_unity(cls, n: int) -> Self: # Instance methods ############################################################################### - def additive_order(self) -> int | np.ndarray: + def additive_order(self) -> int | npt.NDArray: r""" Computes the additive order of each element in $x$. @@ -987,14 +989,14 @@ def additive_order(self) -> int | np.ndarray: field = type(self) if x.ndim == 0: - order = 1 if x == 0 else field.characteristic - else: - order = field.characteristic * np.ones(x.shape, dtype=field.dtypes[-1]) - order[np.where(x == 0)] = 1 + return 1 if x == 0 else field.characteristic + + order = field.characteristic * np.ones(x.shape, dtype=field.dtypes[-1]) + order[np.where(x == 0)] = 1 return order - def multiplicative_order(self) -> int | np.ndarray: + def multiplicative_order(self) -> int | npt.NDArray: r""" Computes the multiplicative order $\textrm{ord}(x)$ of each element in $x$. @@ -1056,7 +1058,7 @@ def multiplicative_order(self) -> int | np.ndarray: return order - def is_square(self) -> bool | np.ndarray: + def is_square(self) -> bool | npt.NDArray: r""" Determines if the elements of $x$ are squares in the finite field. @@ -1655,7 +1657,7 @@ def minimal_poly(self) -> Poly: f"or 2-D to return the minimal polynomial of a square matrix, not have shape {self.shape}." ) - def log(self, base: ElementLike | ArrayLike | None = None) -> int | np.ndarray: + def log(self, base: ElementLike | ArrayLike | None = None) -> int | npt.NDArray: r""" Computes the discrete logarithm of the array $x$ base $\beta$. @@ -1882,7 +1884,7 @@ def _print_power(cls, element: Self) -> str: return s -def _poly_det(A: np.ndarray) -> Poly: +def _poly_det(A: npt.NDArray) -> Poly: """ Computes the determinant of a matrix of `Poly` objects. """ diff --git a/src/galois/_fields/_factory.py b/src/galois/_fields/_factory.py index caf19b1b9..5ed60cdda 100644 --- a/src/galois/_fields/_factory.py +++ b/src/galois/_fields/_factory.py @@ -6,7 +6,7 @@ import sys import types -from typing import Type, overload +from typing import Any, Type, overload from typing_extensions import Literal @@ -48,13 +48,13 @@ def GF( @export def GF( - *args, - irreducible_poly=None, - primitive_element=None, - verify=True, - compile=None, - repr=None, -): + *args: Any, + irreducible_poly: Any = None, + primitive_element: Any = None, + verify: Any = True, + compile: Any = None, + repr: Any = None, +) -> Any: r""" Creates a :obj:`~galois.FieldArray` subclass for $\mathrm{GF}(p^m)$. @@ -88,7 +88,7 @@ def GF( :func:`~galois.FieldArray.compile` method. See :doc:`/basic-usage/compilation-modes` for a further discussion. - - `None` (default): For a newly-created :obj:`~galois.FieldArray` subclass, `None` corresponds to + - `None` (default): For a newly created :obj:`~galois.FieldArray` subclass, `None` corresponds to `"auto"`. If the :obj:`~galois.FieldArray` subclass already exists, `None` does not modify its current compilation mode. - `"auto"`: Selects `"jit-lookup"` for fields with order less than $2^{20}$, `"jit-calculate"` for @@ -109,7 +109,7 @@ def GF( :func:`~galois.FieldArray.repr` method. See :doc:`/basic-usage/element-representation` for a further discussion. - - `None` (default): For a newly-created :obj:`~galois.FieldArray` subclass, `None` corresponds to `"int"`. + - `None` (default): For a newly created :obj:`~galois.FieldArray` subclass, `None` corresponds to `"int"`. If the :obj:`~galois.FieldArray` subclass already exists, `None` does not modify its current element representation. - `"int"`: Sets the element representation to the :ref:`integer representation `. @@ -191,7 +191,7 @@ def GF( GF = galois.GF(3**5, irreducible_poly="x^5 + 2x + 2") print(GF.properties) - Finite fields with arbitrarily-large orders are supported. + Finite fields with arbitrarily large orders are supported. .. md-tab-set:: @@ -246,7 +246,7 @@ def GF( verify_isinstance(order, int) p, e = factors(order) if not len(p) == len(e) == 1: - s = " + ".join([f"{pi}**{ei}" for pi, ei in zip(p, e)]) + s = " * ".join([f"{pi}^{ei}" for pi, ei in zip(p, e)]) raise ValueError(f"Argument 'order' must be a prime power, not {order} = {s}.") characteristic, degree = p[0], e[0] elif len(args) == 2: @@ -325,13 +325,13 @@ def Field( @export def Field( - *args, - irreducible_poly=None, - primitive_element=None, - verify=True, - compile=None, - repr=None, -): + *args: Any, + irreducible_poly: Any = None, + primitive_element: Any = None, + verify: Any = True, + compile: Any = None, + repr: Any = None, +) -> Any: """ Alias of :func:`~galois.GF`. diff --git a/src/galois/_fields/_gf2.py b/src/galois/_fields/_gf2.py index 212feed11..d6ef0ecd2 100644 --- a/src/galois/_fields/_gf2.py +++ b/src/galois/_fields/_gf2.py @@ -90,15 +90,15 @@ class UFuncMixin_2_1(UFuncMixin): def __init_subclass__(cls) -> None: super().__init_subclass__() - cls._add = add_ufunc(cls, override=np.bitwise_xor) - cls._negative = negative_ufunc(cls, override=np.positive) - cls._subtract = subtract_ufunc(cls, override=np.bitwise_xor) - cls._multiply = multiply_ufunc(cls, override=np.bitwise_and) - cls._reciprocal = reciprocal(cls) - cls._divide = divide(cls) - cls._power = power(cls) - cls._log = log(cls) - cls._sqrt = sqrt(cls) + cls._add = add_ufunc(cls, override=np.bitwise_xor) # type: ignore + cls._negative = negative_ufunc(cls, override=np.positive) # type: ignore + cls._subtract = subtract_ufunc(cls, override=np.bitwise_xor) # type: ignore + cls._multiply = multiply_ufunc(cls, override=np.bitwise_and) # type: ignore + cls._reciprocal = reciprocal(cls) # type: ignore + cls._divide = divide(cls) # type: ignore + cls._power = power(cls) # type: ignore + cls._log = log(cls) # type: ignore + cls._sqrt = sqrt(cls) # type: ignore # NOTE: There is a "verbatim" block in the docstring because we were not able to monkey-patch GF2 like the diff --git a/src/galois/_fields/_meta.py b/src/galois/_fields/_meta.py index 64aa37d18..c800d9710 100644 --- a/src/galois/_fields/_meta.py +++ b/src/galois/_fields/_meta.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Type, cast import numpy as np from typing_extensions import Literal @@ -324,7 +324,7 @@ def primitive_elements(cls) -> FieldArray: n = cls.order - 1 powers = np.array(totatives(n)) cls._primitive_elements = np.sort(cls.primitive_element**powers) - return cls._primitive_elements.copy() + return cast(FieldArray, cls._primitive_elements.copy()) @property def squares(cls) -> FieldArray: diff --git a/src/galois/_lfsr.py b/src/galois/_lfsr.py index 8fdb461a4..049615d26 100644 --- a/src/galois/_lfsr.py +++ b/src/galois/_lfsr.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Type, overload +from typing import Any, Callable, Type, cast, overload import numba import numpy as np @@ -18,6 +18,12 @@ from ._polys import Poly from .typing import ArrayLike +ADD: Callable[[int, int], int] +SUBTRACT: Callable[[int, int], int] +MULTIPLY: Callable[[int, int], int] +RECIPROCAL: Callable[[int], int] + + ############################################################################### # LFSR base class ############################################################################### @@ -39,7 +45,7 @@ def __init__( if not feedback_poly.coeffs[-1] == 1: raise ValueError(f"Argument 'feedback_poly' must have a 0-th degree term of 1, not {feedback_poly}.") - self._field = feedback_poly.field + self._field = cast(type[FieldArray], feedback_poly.field) self._feedback_poly = feedback_poly self._characteristic_poly = feedback_poly.reverse() self._order = feedback_poly.degree @@ -47,11 +53,12 @@ def __init__( if self._type == "fibonacci": # T = [c_n-1, c_n-2, ..., c_1, c_0] # c(x) = x^{n} - c_{n-1}x^{n-1} - c_{n-2}x^{n-2} - \dots - c_{1}x - c_{0} - self._taps = -self.characteristic_poly.coeffs[1:] + taps = -self.characteristic_poly.coeffs[1:] else: # T = [c_0, c_1, ..., c_n-2, c_n-1] # c(x) = x^{n} - c_{n-1}x^{n-1} - c_{n-2}x^{n-2} - \dots - c_{1}x - c_{0} - self._taps = -self.characteristic_poly.coeffs[1:][::-1] + taps = -self.characteristic_poly.coeffs[1:][::-1] + self._taps = cast(FieldArray, taps) if state is None: state = self.field.Ones(self.order) @@ -108,13 +115,14 @@ def step(self, steps: int = 1) -> FieldArray: return y - def _step_forward(self, steps): + def _step_forward(self, steps: int) -> FieldArray: assert steps > 0 if self._type == "fibonacci": y, state = fibonacci_lfsr_step_forward_jit(self.field)(self.taps, self.state, steps) else: y, state = galois_lfsr_step_forward_jit(self.field)(self.taps, self.state, steps) + y = cast(FieldArray, y) self._state[:] = state[:] if y.size == 1: @@ -122,7 +130,7 @@ def _step_forward(self, steps): return y - def _step_backward(self, steps): + def _step_backward(self, steps: int) -> FieldArray: assert steps > 0 if not self.characteristic_poly.coeffs[-1] > 0: @@ -135,6 +143,7 @@ def _step_backward(self, steps): y, state = fibonacci_lfsr_step_backward_jit(self.field)(self.taps, self.state, steps) else: y, state = galois_lfsr_step_backward_jit(self.field)(self.taps, self.state, steps) + y = cast(FieldArray, y) self._state[:] = state[:] if y.size == 1: @@ -1422,7 +1431,7 @@ def berlekamp_massey(sequence: FieldArray, output: Literal["galois"]) -> GLFSR: @export -def berlekamp_massey(sequence, output="minimal"): +def berlekamp_massey(sequence: Any, output: Any = "minimal") -> Any: r""" Finds the minimal polynomial $c(x)$ that produces the linear recurrent sequence $y$. diff --git a/src/galois/_modular.py b/src/galois/_modular.py index 26e4ae021..8ef33ff12 100644 --- a/src/galois/_modular.py +++ b/src/galois/_modular.py @@ -82,7 +82,7 @@ def euler_phi(n: int) -> int: Notes: This function implements the Euler totient function - $$\phi(n) = n \prod_{p\ |\ n} \bigg(1 - \frac{1}{p}\bigg) = \prod_{i=1}^{k} p_i^{e_i-1} \big(p_i - 1\big)$$ + $$\phi(n) = n \prod_{p \mid n} \bigg(1 - \frac{1}{p}\bigg) = \prod_{i=1}^{k} p_i^{e_i-1} \big(p_i - 1\big)$$ for prime $p$ and the prime factorization $n = p_1^{e_1} \dots p_k^{e_k}$. diff --git a/src/galois/_ntt.py b/src/galois/_ntt.py index 601e2f56b..65bcf1895 100644 --- a/src/galois/_ntt.py +++ b/src/galois/_ntt.py @@ -4,6 +4,8 @@ from __future__ import annotations +from typing import cast + import numpy as np from ._fields import Field, FieldArray @@ -236,7 +238,14 @@ def intt( return _ntt(X, size=size, modulus=modulus, forward=False, scaled=scaled) -def _ntt(x, size=None, modulus=None, forward=True, scaled=True): +def _ntt( + x: ArrayLike, + size: int | None = None, + modulus: int | None = None, + forward: bool = True, + scaled: bool = True, +) -> FieldArray: + x = np.asarray(x) verify_isinstance(size, int, optional=True) verify_isinstance(modulus, int, optional=True) verify_isinstance(forward, bool) @@ -274,5 +283,6 @@ def _ntt(x, size=None, modulus=None, forward=True, scaled=True): else: norm = "backward" if scaled else "forward" y = np.fft.ifft(x, n=size, norm=norm) + y = cast(FieldArray, y) return y diff --git a/src/galois/_polymorphic.py b/src/galois/_polymorphic.py index d3d711cda..2805f131a 100644 --- a/src/galois/_polymorphic.py +++ b/src/galois/_polymorphic.py @@ -4,9 +4,7 @@ from __future__ import annotations -from typing import Sequence, overload - -import numpy as np +from typing import Any, Sequence, overload from ._helper import export from ._math import egcd as int_egcd @@ -35,7 +33,7 @@ def gcd(a: Poly, b: Poly) -> Poly: ... @export -def gcd(a, b): +def gcd(a: Any, b: Any) -> Any: r""" Finds the greatest common divisor of $a$ and $b$. @@ -87,7 +85,7 @@ def gcd(a, b): Group: number-theory-divisibility """ - if isinstance(a, (int, np.integer)) and isinstance(b, (int, np.integer)): + if isinstance(a, int) and isinstance(b, int): return int_gcd(a, b) if isinstance(a, Poly) and isinstance(b, Poly): return poly_gcd(a, b) @@ -103,7 +101,7 @@ def egcd(a: Poly, b: Poly) -> tuple[Poly, Poly, Poly]: ... @export -def egcd(a, b): +def egcd(a: Any, b: Any) -> Any: r""" Finds the multiplicands of $a$ and $b$ such that $a s + b t = \mathrm{gcd}(a, b)$. @@ -165,7 +163,7 @@ def egcd(a, b): Group: number-theory-divisibility """ - if isinstance(a, (int, np.integer)) and isinstance(b, (int, np.integer)): + if isinstance(a, int) and isinstance(b, int): return int_egcd(a, b) if isinstance(a, Poly) and isinstance(b, Poly): return poly_egcd(a, b) @@ -181,7 +179,7 @@ def lcm(*values: Poly) -> Poly: ... @export -def lcm(*values): +def lcm(*values: Any) -> Any: r""" Computes the least common multiple of the arguments. @@ -230,7 +228,7 @@ def lcm(*values): if not len(values) > 0: raise ValueError("At least one argument must be provided.") - if all(isinstance(value, (int, np.integer)) for value in values): + if all(isinstance(value, int) for value in values): return int_lcm(*values) if all(isinstance(value, Poly) for value in values): return poly_lcm(*values) @@ -246,7 +244,7 @@ def prod(*values: Poly) -> Poly: ... @export -def prod(*values): +def prod(*values: Any) -> Any: r""" Computes the product of the arguments. @@ -294,7 +292,7 @@ def prod(*values): if not len(values) > 0: raise ValueError("At least one argument must be provided.") - if all(isinstance(value, (int, np.integer)) for value in values): + if all(isinstance(value, int) for value in values): return int_prod(*values) if all(isinstance(value, Poly) for value in values): return poly_prod(*values) @@ -310,7 +308,7 @@ def are_coprime(*values: Poly) -> bool: ... @export -def are_coprime(*values): +def are_coprime(*values: Any) -> Any: r""" Determines if the arguments are pairwise coprime. @@ -359,10 +357,7 @@ def are_coprime(*values): Group: number-theory-divisibility """ - if not ( - all(isinstance(value, (int, np.integer)) for value in values) - or all(isinstance(value, Poly) for value in values) - ): + if not (all(isinstance(value, int) for value in values) or all(isinstance(value, Poly) for value in values)): raise TypeError(f"All arguments must be either int or galois.Poly, not {[type(value) for value in values]}.") if not len(values) > 0: raise ValueError("At least one argument must be provided.") @@ -384,7 +379,7 @@ def crt(remainders: Sequence[Poly], moduli: Sequence[Poly]) -> Poly: ... @export -def crt(remainders, moduli): +def crt(remainders: Any, moduli: Any) -> Any: r""" Solves the simultaneous system of congruences for $x$. @@ -467,12 +462,12 @@ def crt(remainders, moduli): """ if not ( isinstance(remainders, (tuple, list)) - and (all(isinstance(x, (int, np.integer)) for x in remainders) or all(isinstance(x, Poly) for x in remainders)) + and (all(isinstance(x, int) for x in remainders) or all(isinstance(x, Poly) for x in remainders)) ): raise TypeError(f"Argument 'remainders' must be a tuple or list of int or Poly, not {remainders}.") if not ( isinstance(moduli, (tuple, list)) - and (all(isinstance(x, (int, np.integer)) for x in moduli) or all(isinstance(x, Poly) for x in moduli)) + and (all(isinstance(x, int) for x in moduli) or all(isinstance(x, Poly) for x in moduli)) ): raise TypeError(f"Argument 'moduli' must be a tuple or list of int or Poly, not {moduli}.") if not len(remainders) == len(moduli) >= 2: @@ -535,7 +530,7 @@ def factors(value: Poly) -> tuple[list[Poly], list[int]]: ... @export -def factors(value): +def factors(value: Any) -> Any: r""" Computes the prime factors of a positive integer or the irreducible factors of a non-constant, monic polynomial. @@ -633,7 +628,7 @@ def factors(value): Group: factorization-prime """ - if isinstance(value, (int, np.integer)): + if isinstance(value, int): return int_factors(value) if isinstance(value, Poly): return value.factors() @@ -649,7 +644,7 @@ def is_square_free(value: Poly) -> bool: ... @export -def is_square_free(value): +def is_square_free(value: Any) -> Any: r""" Determines if an integer or polynomial is square-free. @@ -713,7 +708,7 @@ def is_square_free(value): Group: primes-tests """ - if isinstance(value, (int, np.integer)): + if isinstance(value, int): return int_is_square_free(value) if isinstance(value, Poly): return value.is_square_free() diff --git a/src/galois/_polys/_conversions.py b/src/galois/_polys/_conversions.py index 18aeafac1..45450a49c 100644 --- a/src/galois/_polys/_conversions.py +++ b/src/galois/_polys/_conversions.py @@ -5,8 +5,6 @@ from __future__ import annotations -import numpy as np - from .._math import ilog from .._options import get_printoptions @@ -80,7 +78,7 @@ def poly_to_str(coeffs: list[int], poly_var: str = "x") -> str: """ Converts the polynomial coefficients (descending order) into its string representation. """ - degrees = np.arange(len(coeffs) - 1, -1, -1) + degrees = list(range(len(coeffs) - 1, -1, -1)) return sparse_poly_to_str(degrees, coeffs, poly_var=poly_var) @@ -151,29 +149,29 @@ def str_to_sparse_poly(poly_str: str) -> tuple[list[int], list[int]]: coeffs = [] for element in s.split("+"): if var not in element: - degree = 0 - coeff = element + degree_str = "0" + coeff_str = element elif "^" not in element and "**" not in element: - degree = 1 - coeff = element.split(var, 1)[0] + degree_str = "1" + coeff_str = element.split(var, 1)[0] elif "^" in element: - coeff, degree = element.split(var + "^", 1) + coeff_str, degree_str = element.split(var + "^", 1) elif "**" in element: - coeff, degree = element.split(var + "**", 1) + coeff_str, degree_str = element.split(var + "**", 1) else: raise ValueError(f"Could not parse polynomial degree in {element}.") # If the degree was negative `3*x^-2`, it was converted to `3*x^+-2` previously. When split # by "+" it will leave the degree empty. - if degree == "": + if degree_str == "": raise ValueError(f"Cannot parse polynomials with negative exponents, {poly_str!r}.") - degree = int(degree) + degree = int(degree_str) - coeff = coeff.replace("*", "") # Remove multiplication sign for elements like `3*x^2` - if coeff == "-": + coeff_str = coeff_str.replace("*", "") # Remove multiplication sign for elements like `3*x^2` + if coeff_str == "-": coeff = -1 - elif coeff != "": - coeff = int(coeff) + elif coeff_str != "": + coeff = int(coeff_str) else: coeff = 1 diff --git a/src/galois/_polys/_conway.py b/src/galois/_polys/_conway.py index 59c81c99c..23710c929 100644 --- a/src/galois/_polys/_conway.py +++ b/src/galois/_polys/_conway.py @@ -24,7 +24,7 @@ def is_conway(f: Poly, search: bool = False) -> bool: .. question:: Why is this a method and not a property? :collapsible: - This is a method to indicate it is a computationally-expensive task. + This is a method to indicate it is a computationally expensive task. Arguments: search: Manually search for Conway polynomials if they are not included in `Frank Luebeck's database @@ -47,10 +47,10 @@ def is_conway(f: Poly, search: bool = False) -> bool: Notes: A degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(p)$ is the *Conway polynomial* $C_{p,m}(x)$ if it is monic, primitive, compatible with Conway polynomials $C_{p,n}(x)$ for all - $n\ |\ m$, and is lexicographically first according to a special ordering. + $n \mid m$, and is lexicographically first according to a special ordering. A Conway polynomial $C_{p,m}(x)$ is *compatible* with Conway polynomials $C_{p,n}(x)$ for - $n\ |\ m$ if $C_{p,n}(x^r)$ divides $C_{p,m}(x)$, where $r = \frac{p^m - 1}{p^n - 1}$. + $n \mid m$ if $C_{p,n}(x^r)$ divides $C_{p,m}(x)$, where $r = \frac{p^m - 1}{p^n - 1}$. The Conway lexicographic ordering is defined as follows. Given two degree-$m$ polynomials $g(x) = \sum_{i=0}^m g_i x^i$ and $h(x) = \sum_{i=0}^m h_i x^i$, then $g < h$ if and only if @@ -81,7 +81,7 @@ def is_conway(f: Poly, search: bool = False) -> bool: f.is_conway_consistent() g.is_conway_consistent() - Among the multiple candidate Conway polynomials, the lexicographically-first (accordingly to a special + Among the multiple candidate Conway polynomials, the lexicographically first (accordingly to a special lexicographical order) is the Conway polynomial. .. ipython:: python @@ -106,12 +106,12 @@ def is_conway(f: Poly, search: bool = False) -> bool: def is_conway_consistent(f: Poly, search: bool = False) -> bool: r""" Determines whether the degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(p)$ is consistent - with smaller Conway polynomials $C_{p,n}(x)$ for all $n\ |\ m$. + with smaller Conway polynomials $C_{p,n}(x)$ for all $n \mid m$. .. question:: Why is this a method and not a property? :collapsible: - This is a method to indicate it is a computationally-expensive task. + This is a method to indicate it is a computationally expensive task. Arguments: search: Manually search for Conway polynomials if they are not included in `Frank Luebeck's database @@ -123,7 +123,7 @@ def is_conway_consistent(f: Poly, search: bool = False) -> bool: Returns: `True` if the polynomial $f(x)$ is primitive and consistent with smaller Conway polynomials - $C_{p,n}(x)$ for all $n\ |\ m$. + $C_{p,n}(x)$ for all $n \mid m$. Raises: LookupError: If `search=False` and a smaller Conway polynomial $C_{p,n}$ is not found in Frank Luebeck's @@ -134,7 +134,7 @@ def is_conway_consistent(f: Poly, search: bool = False) -> bool: Notes: A degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(p)$ is *compatible* with Conway polynomials - $C_{p,n}(x)$ for $n\ |\ m$ if $C_{p,n}(x^r)$ divides $f(x)$, where + $C_{p,n}(x)$ for $n \mid m$ if $C_{p,n}(x^r)$ divides $f(x)$, where $r = \frac{p^m - 1}{p^n - 1}$. A Conway-consistent polynomial has all the properties of a Conway polynomial except that it is not @@ -164,7 +164,7 @@ def is_conway_consistent(f: Poly, search: bool = False) -> bool: f.is_conway_consistent() g.is_conway_consistent() - Among the multiple candidate Conway polynomials, the lexicographically-first (accordingly to a special + Among the multiple candidate Conway polynomials, the lexicographically first (accordingly to a special lexicographical order) is the Conway polynomial. .. ipython:: python @@ -230,10 +230,10 @@ def conway_poly(characteristic: int, degree: int, search: bool = False) -> Poly: Notes: A degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(p)$ is the *Conway polynomial* $C_{p,m}(x)$ if it is monic, primitive, compatible with Conway polynomials $C_{p,n}(x)$ for all - $n\ |\ m$, and is lexicographically first according to a special ordering. + $n \mid m$, and is lexicographically first according to a special ordering. A Conway polynomial $C_{p,m}(x)$ is *compatible* with Conway polynomials $C_{p,n}(x)$ for - $n\ |\ m$ if $C_{p,n}(x^r)$ divides $C_{p,m}(x)$, where $r = \frac{p^m - 1}{p^n - 1}$. + $n \mid m$ if $C_{p,n}(x^r)$ divides $C_{p,m}(x)$, where $r = \frac{p^m - 1}{p^n - 1}$. The Conway lexicographic ordering is defined as follows. Given two degree-$m$ polynomials $g(x) = \sum_{i=0}^m g_i x^i$ and $h(x) = \sum_{i=0}^m h_i x^i$, then $g < h$ if and only if @@ -268,7 +268,7 @@ def conway_poly(characteristic: int, degree: int, search: bool = False) -> Poly: f.is_conway_consistent() g.is_conway_consistent() - Among the multiple candidate Conway polynomials, the lexicographically-first (accordingly to a special + Among the multiple candidate Conway polynomials, the lexicographically first (accordingly to a special lexicographical order) is the Conway polynomial. .. ipython:: python diff --git a/src/galois/_polys/_dense.py b/src/galois/_polys/_dense.py index 6edd1e80e..ae6ce4e7f 100644 --- a/src/galois/_polys/_dense.py +++ b/src/galois/_polys/_dense.py @@ -4,14 +4,27 @@ from __future__ import annotations +from typing import Callable, cast + import numba import numpy as np +import numpy.typing as npt from numba import int64, uint64 from .._domains import Array from .._domains._function import Function from .._helper import verify_isinstance +ORDER: int + +ADD: Callable[[int, int], int] +SUBTRACT: Callable[[int, int], int] +MULTIPLY: Callable[[int, int], int] +RECIPROCAL: Callable[[int], int] +POWER: Callable[[int, int], int] +POLY_MULTIPLY: Callable[[npt.NDArray, npt.NDArray], npt.NDArray] +POLY_MOD: Callable[[npt.NDArray, npt.NDArray], npt.NDArray] + class add_jit(Function): """ @@ -120,7 +133,7 @@ def multiply(a: Array, b: Array) -> Array: if a.ndim == 0 or b.ndim == 0: return a * b - return np.convolve(a, b) + return cast(Array, np.convolve(a, b)) class divmod_jit(Function): @@ -149,7 +162,7 @@ def __call__(self, a: Array, b: Array) -> tuple[Array, Array]: assert 1 <= a.ndim <= 2 and b.ndim == 1 dtype = a.dtype a_1d = a.ndim == 1 - a = np.atleast_2d(a) + a = cast(Array, np.atleast_2d(a)) # TODO: Do not support 2D -- it is no longer needed q_degree = a.shape[-1] - b.shape[-1] @@ -166,8 +179,8 @@ def __call__(self, a: Array, b: Array) -> tuple[Array, Array]: r = qr[:, q_degree + 1 : q_degree + 1 + r_degree + 1] if a_1d: - q = q.reshape(q.size) - r = r.reshape(r.size) + q = cast(Array, q.reshape(q.size)) + r = cast(Array, r.reshape(r.size)) return q, r @@ -335,7 +348,7 @@ def __call__(self, a: Array, b: int, c: Array | None = None) -> Array: assert a.ndim == 1 and c.ndim == 1 if c is not None else True dtype = a.dtype - # Convert the integer b into a vector of uint64 [MSWord, ..., LSWord] so arbitrarily-large exponents may be + # Convert the integer b into a vector of uint64 [MSWord, ..., LSWord] so arbitrarily large exponents may be # passed into the JIT-compiled version b_vec = [] # Pop on LSWord -> MSWord while b >= 2**64: @@ -366,7 +379,7 @@ def set_globals(self): @staticmethod def implementation(a, b_vec, c): """ - b is a vector of uint64 [MSWord, ..., LSWord] so that arbitrarily-large exponents may be passed + b is a vector of uint64 [MSWord, ..., LSWord] so that arbitrarily large exponents may be passed """ if b_vec.size == 1 and b_vec[0] == 0: return np.array([1], dtype=a.dtype) @@ -445,7 +458,7 @@ class roots_jit(Function): Finds the roots of the polynomial f(x). """ - def __call__(self, nonzero_degrees: np.ndarray, nonzero_coeffs: Array) -> Array: + def __call__(self, nonzero_degrees: npt.NDArray, nonzero_coeffs: Array) -> Array: verify_isinstance(nonzero_degrees, np.ndarray) verify_isinstance(nonzero_coeffs, self.field) dtype = nonzero_coeffs.dtype diff --git a/src/galois/_polys/_factor.py b/src/galois/_polys/_factor.py index 3922edfed..59a440630 100644 --- a/src/galois/_polys/_factor.py +++ b/src/galois/_polys/_factor.py @@ -19,7 +19,7 @@ def is_square_free(f) -> bool: .. question:: Why is this a method and not a property? :collapsible: - This is a method to indicate it is a computationally-expensive task. + This is a method to indicate it is a computationally expensive task. Returns: `True` if the polynomial is square-free. diff --git a/src/galois/_polys/_irreducible.py b/src/galois/_polys/_irreducible.py index 9fd0828b3..e0541ef43 100644 --- a/src/galois/_polys/_irreducible.py +++ b/src/galois/_polys/_irreducible.py @@ -33,7 +33,7 @@ def is_irreducible(f: Poly) -> bool: .. question:: Why is this a method and not a property? :collapsible: - This is a method to indicate it is a computationally-expensive task. + This is a method to indicate it is a computationally expensive task. Returns: `True` if the polynomial is irreducible. @@ -49,7 +49,7 @@ def is_irreducible(f: Poly) -> bool: This function implements Rabin's irreducibility test. It says a degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(q)$ for prime power $q$ is irreducible if and only if - $f(x)\ |\ (x^{q^m} - x)$ and $\textrm{gcd}(f(x),\ x^{q^{m_i}} - x) = 1$ for + $f(x) \mid (x^{q^m} - x)$ and $\textrm{gcd}(f(x),\ x^{q^{m_i}} - x) = 1$ for $1 \le i \le k$, where $m_i = m/p_i$ for the $k$ prime divisors $p_i$ of $m$. References: @@ -145,8 +145,8 @@ def irreducible_poly( method: The search method for finding the irreducible polynomial. - - `"min"` (default): Returns the lexicographically-first polynomial. - - `"max"`: Returns the lexicographically-last polynomial. + - `"min"` (default): Returns the lexicographically first polynomial. + - `"max"`: Returns the lexicographically last polynomial. - `"random"`: Returns a random polynomial. .. fast-performance:: @@ -176,7 +176,7 @@ def irreducible_poly( $\mathrm{GF}(q)$. Examples: - Find the lexicographically-first, lexicographically-last, and a random monic irreducible polynomial. + Find the lexicographically first, lexicographically last, and a random monic irreducible polynomial. .. ipython:: python @@ -184,13 +184,13 @@ def irreducible_poly( galois.irreducible_poly(7, 3, method="max") galois.irreducible_poly(7, 3, method="random") - Find the lexicographically-first monic irreducible polynomial with four terms. + Find the lexicographically first monic irreducible polynomial with four terms. .. ipython:: python galois.irreducible_poly(7, 3, terms=4) - Find the lexicographically-first monic irreducible polynomial with the minimum number of non-zero terms. + Find the lexicographically first monic irreducible polynomial with the minimum number of non-zero terms. .. ipython:: python diff --git a/src/galois/_polys/_poly.py b/src/galois/_polys/_poly.py index 36c12a203..7dd155cf5 100644 --- a/src/galois/_polys/_poly.py +++ b/src/galois/_polys/_poly.py @@ -4,9 +4,10 @@ from __future__ import annotations -from typing import Sequence, Type, overload +from typing import Any, Sequence, Type, cast, overload import numpy as np +import numpy.typing as npt from typing_extensions import Literal, Self from .._domains import Array, _factory @@ -59,9 +60,9 @@ class Poly: # "binary", and "sparse". All types define _field, "dense" defines _coeffs, "binary" defines "_integer", and # "sparse" defines _nonzero_degrees and _nonzero_coeffs. The other properties are created when needed. _field: Type[Array] - _degrees: np.ndarray + _degrees: npt.NDArray _coeffs: Array - _nonzero_degrees: np.ndarray + _nonzero_degrees: npt.NDArray _nonzero_coeffs: Array _integer: int _degree: int @@ -110,11 +111,11 @@ def __init__( self._coeffs, self._field = _convert_coeffs(coeffs, field) if self._coeffs.ndim == 0: - self._coeffs = np.atleast_1d(self._coeffs) + self._coeffs = cast(Array, np.atleast_1d(self._coeffs)) if order == "asc": - self._coeffs = np.flip(self._coeffs) # Ensure it's in descending-degree order + self._coeffs = cast(Array, np.flip(self._coeffs)) # Ensure it's in descending-degree order if self._coeffs[0] == 0: - self._coeffs = np.trim_zeros(self._coeffs, "f") # Remove leading zeros + self._coeffs = cast(Array, np.trim_zeros(self._coeffs, "f")) # Remove leading zeros if self._coeffs.size == 0: self._coeffs = self._field([0]) @@ -132,12 +133,12 @@ def _PolyLike(cls, poly_like: PolyLike, field: Type[Array] | None = None) -> Sel A private alternate constructor that converts a poly-like object into a polynomial, given a finite field. """ if isinstance(poly_like, int): - poly = Poly.Int(poly_like, field=field) + poly = cls.Int(poly_like, field=field) elif isinstance(poly_like, str): - poly = Poly.Str(poly_like, field=field) + poly = cls.Str(poly_like, field=field) elif isinstance(poly_like, (tuple, list, np.ndarray)): - poly = Poly(poly_like, field=field) - elif isinstance(poly_like, Poly): + poly = cls(poly_like, field=field) + elif isinstance(poly_like, cls): poly = poly_like else: raise TypeError( @@ -177,7 +178,7 @@ def Zero(cls, field: Type[Array] | None = None) -> Self: GF = galois.GF(3**5) galois.Poly.Zero(GF) """ - return Poly([0], field=field) + return cls([0], field=field) @classmethod def One(cls, field: Type[Array] | None = None) -> Self: @@ -205,7 +206,7 @@ def One(cls, field: Type[Array] | None = None) -> Self: GF = galois.GF(3**5) galois.Poly.One(GF) """ - return Poly([1], field=field) + return cls([1], field=field) @classmethod def Identity(cls, field: Type[Array] | None = None) -> Self: @@ -233,7 +234,7 @@ def Identity(cls, field: Type[Array] | None = None) -> Self: GF = galois.GF(3**5) galois.Poly.Identity(GF) """ - return Poly([1, 0], field=field) + return cls([1, 0], field=field) @classmethod def Random( @@ -298,7 +299,7 @@ def Random( if coeffs[0] == 0: coeffs[0] = field.Random(low=1, seed=rng) # Ensure leading coefficient is non-zero - return Poly(coeffs) + return cls(coeffs) @classmethod def Str(cls, string: str, field: Type[Array] | None = None) -> Self: @@ -348,7 +349,7 @@ def Str(cls, string: str, field: Type[Array] | None = None) -> Self: degrees, coeffs = str_to_sparse_poly(string) - return Poly.Degrees(degrees, coeffs, field=field) + return cls.Degrees(degrees, coeffs, field=field) @classmethod def Int(cls, integer: int, field: Type[Array] | None = None) -> Self: @@ -428,7 +429,7 @@ def Int(cls, integer: int, field: Type[Array] | None = None) -> Self: @classmethod def Degrees( cls, - degrees: Sequence[int] | np.ndarray, + degrees: Sequence[int] | npt.NDArray, coeffs: ArrayLike | None = None, field: Type[Array] | None = None, ) -> Self: @@ -518,7 +519,7 @@ def Degrees( def Roots( cls, roots: ArrayLike, - multiplicities: Sequence[int] | np.ndarray | None = None, + multiplicities: Sequence[int] | npt.NDArray | None = None, field: Type[Array] | None = None, ) -> Self: r""" @@ -586,8 +587,8 @@ def Roots( f"not {len(roots)} and {len(multiplicities)}." ) - poly = Poly.One(field=field) - x = Poly.Identity(field=field) + poly = cls.One(field=field) + x = cls.Identity(field=field) for root, multiplicity in zip(roots, multiplicities): poly *= (x - root) ** multiplicity @@ -655,7 +656,7 @@ def coefficients( coeffs = self.field.Zeros(size) coeffs[-len(self) :] = self.coeffs if order == "asc": - coeffs = np.flip(coeffs) + coeffs = cast(Array, np.flip(coeffs)) return coeffs @@ -688,9 +689,9 @@ def reverse(self) -> Poly: def roots(self, multiplicity: Literal[False] = False) -> Array: ... @overload - def roots(self, multiplicity: Literal[True]) -> tuple[Array, np.ndarray]: ... + def roots(self, multiplicity: Literal[True]) -> tuple[Array, npt.NDArray]: ... - def roots(self, multiplicity=False): + def roots(self, multiplicity: Any = False) -> Any: r""" Calculates the roots $r$ of the polynomial $f(x)$, such that $f(r) = 0$. @@ -903,7 +904,7 @@ def is_conway(self) -> bool: def is_conway_consistent(self) -> bool: r""" Determines whether the degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(p)$ is consistent - with smaller Conway polynomials $C_{p,n}(x)$ for all $n\ |\ m$. + with smaller Conway polynomials $C_{p,n}(x)$ for all $n \mid m$. """ # Will be monkey-patched in `_conway.py` raise NotImplementedError @@ -1007,7 +1008,7 @@ def __call__( @overload def __call__(self, at: Poly) -> Poly: ... - def __call__(self, at, field=None, elementwise=True): + def __call__(self, at: Any, field: Any = None, elementwise: Any = True) -> Any: r""" Evaluates the polynomial $f(x)$ at $x_0$ or the polynomial composition $f(g(x))$. @@ -1163,10 +1164,10 @@ def __add__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(other, self.field) - c = _binary.add(a, b) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(other, self.field) + c_int = _binary.add(a_int, b_int) + output = Poly.Int(c_int, field=self.field) elif "sparse" in types: a_degrees, a_coeffs = _convert_to_sparse_coeffs(self, self.field) b_degrees, b_coeffs = _convert_to_sparse_coeffs(other, self.field) @@ -1185,10 +1186,10 @@ def __radd__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(other, self.field) - b = _convert_to_integer(self, self.field) - c = _binary.add(a, b) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(other, self.field) + b_int = _convert_to_integer(self, self.field) + c_int = _binary.add(a_int, b_int) + output = Poly.Int(c_int, field=self.field) elif "sparse" in types: a_degrees, a_coeffs = _convert_to_sparse_coeffs(other, self.field) b_degrees, b_coeffs = _convert_to_sparse_coeffs(self, self.field) @@ -1204,9 +1205,9 @@ def __radd__(self, other: Poly | Array) -> Poly: def __neg__(self): if self._type == "binary": - a = _convert_to_integer(self, self.field) - c = _binary.negative(a) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(self, self.field) + c_int = _binary.negative(a_int) + output = Poly.Int(c_int, field=self.field) elif self._type == "sparse": a_degrees, a_coeffs = _convert_to_sparse_coeffs(self, self.field) c_degrees, c_coeffs = _sparse.negative(a_degrees, a_coeffs) @@ -1223,10 +1224,10 @@ def __sub__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(other, self.field) - c = _binary.subtract(a, b) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(other, self.field) + c_int = _binary.subtract(a_int, b_int) + output = Poly.Int(c_int, field=self.field) elif "sparse" in types: a_degrees, a_coeffs = _convert_to_sparse_coeffs(self, self.field) b_degrees, b_coeffs = _convert_to_sparse_coeffs(other, self.field) @@ -1245,10 +1246,10 @@ def __rsub__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(other, self.field) - b = _convert_to_integer(self, self.field) - c = _binary.subtract(a, b) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(other, self.field) + b_int = _convert_to_integer(self, self.field) + c_int = _binary.subtract(a_int, b_int) + output = Poly.Int(c_int, field=self.field) elif "sparse" in types: a_degrees, a_coeffs = _convert_to_sparse_coeffs(other, self.field) b_degrees, b_coeffs = _convert_to_sparse_coeffs(self, self.field) @@ -1267,10 +1268,10 @@ def __mul__(self, other: Poly | Array | int) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(other, self.field) - c = _binary.multiply(a, b) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(other, self.field) + c_int = _binary.multiply(a_int, b_int) + output = Poly.Int(c_int, field=self.field) elif "sparse" in types: a_degrees, a_coeffs = _convert_to_sparse_coeffs(self, self.field) b_degrees, b_coeffs = _convert_to_sparse_coeffs(other, self.field) @@ -1289,10 +1290,10 @@ def __rmul__(self, other: Poly | Array | int) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(other, self.field) - b = _convert_to_integer(self, self.field) - c = _binary.multiply(a, b) - output = Poly.Int(c, field=self.field) + a_int = _convert_to_integer(other, self.field) + b_int = _convert_to_integer(self, self.field) + c_int = _binary.multiply(a_int, b_int) + output = Poly.Int(c_int, field=self.field) elif "sparse" in types: a_degrees, a_coeffs = _convert_to_sparse_coeffs(other, self.field) b_degrees, b_coeffs = _convert_to_sparse_coeffs(self, self.field) @@ -1311,10 +1312,10 @@ def __divmod__(self, other: Poly | Array) -> tuple[Poly, Poly]: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(other, self.field) - q, r = _binary.divmod(a, b) - output = Poly.Int(q, field=self.field), Poly.Int(r, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(other, self.field) + q_int, r_int = _binary.divmod(a_int, b_int) + output = Poly.Int(q_int, field=self.field), Poly.Int(r_int, field=self.field) else: a = _convert_to_coeffs(self, self.field) b = _convert_to_coeffs(other, self.field) @@ -1328,10 +1329,10 @@ def __rdivmod__(self, other: Poly | Array) -> tuple[Poly, Poly]: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(other, self.field) - b = _convert_to_integer(self, self.field) - q, r = _binary.divmod(a, b) - output = Poly.Int(q, field=self.field), Poly.Int(r, field=self.field) + a_int = _convert_to_integer(other, self.field) + b_int = _convert_to_integer(self, self.field) + q_int, r_int = _binary.divmod(a_int, b_int) + output = Poly.Int(q_int, field=self.field), Poly.Int(r_int, field=self.field) else: a = _convert_to_coeffs(other, self.field) b = _convert_to_coeffs(self, self.field) @@ -1357,10 +1358,10 @@ def __floordiv__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(other, self.field) - q = _binary.floordiv(a, b) - output = Poly.Int(q, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(other, self.field) + q_int = _binary.floordiv(a_int, b_int) + output = Poly.Int(q_int, field=self.field) else: a = _convert_to_coeffs(self, self.field) b = _convert_to_coeffs(other, self.field) @@ -1374,10 +1375,10 @@ def __rfloordiv__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(other, self.field) - b = _convert_to_integer(self, self.field) - q = _binary.floordiv(a, b) - output = Poly.Int(q, field=self.field) + a_int = _convert_to_integer(other, self.field) + b_int = _convert_to_integer(self, self.field) + q_int = _binary.floordiv(a_int, b_int) + output = Poly.Int(q_int, field=self.field) else: a = _convert_to_coeffs(other, self.field) b = _convert_to_coeffs(self, self.field) @@ -1391,10 +1392,10 @@ def __mod__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(other, self.field) - r = _binary.mod(a, b) - output = Poly.Int(r, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(other, self.field) + r_int = _binary.mod(a_int, b_int) + output = Poly.Int(r_int, field=self.field) else: a = _convert_to_coeffs(self, self.field) b = _convert_to_coeffs(other, self.field) @@ -1408,10 +1409,10 @@ def __rmod__(self, other: Poly | Array) -> Poly: types = [getattr(self, "_type", None), getattr(other, "_type", None)] if "binary" in types: - a = _convert_to_integer(other, self.field) - b = _convert_to_integer(self, self.field) - r = _binary.mod(a, b) - output = Poly.Int(r, field=self.field) + a_int = _convert_to_integer(other, self.field) + b_int = _convert_to_integer(self, self.field) + r_int = _binary.mod(a_int, b_int) + output = Poly.Int(r_int, field=self.field) else: a = _convert_to_coeffs(other, self.field) b = _convert_to_coeffs(self, self.field) @@ -1429,10 +1430,10 @@ def __pow__(self, exponent: int, modulus: Poly | None = None) -> Poly: raise ValueError(f"Can only exponentiate polynomials to non-negative integers, not {exponent}.") if "binary" in types: - a = _convert_to_integer(self, self.field) - b = _convert_to_integer(modulus, self.field) if modulus is not None else None - q = _binary.pow(a, exponent, b) - output = Poly.Int(q, field=self.field) + a_int = _convert_to_integer(self, self.field) + b_int = _convert_to_integer(modulus, self.field) if modulus is not None else None + q_int = _binary.pow(a_int, exponent, b_int) + output = Poly.Int(q_int, field=self.field) else: a = _convert_to_coeffs(self, self.field) b = _convert_to_coeffs(modulus, self.field) if modulus is not None else None @@ -1577,7 +1578,7 @@ def nonzero_coeffs(self) -> Array: return self._nonzero_coeffs.copy() @property - def nonzero_degrees(self) -> np.ndarray: + def nonzero_degrees(self) -> npt.NDArray: """ An array of the polynomial degrees that have non-zero coefficients in descending order. @@ -1646,7 +1647,7 @@ def _convert_coeffs(coeffs: ArrayLike, field: Type[Array] | None = None) -> tupl field = _factory.DEFAULT_ARRAY coeffs = np.array(coeffs, dtype=field.dtypes[-1]) sign = np.sign(coeffs) - coeffs = sign * field(np.abs(coeffs)) + coeffs = cast(Array, sign * field(np.abs(coeffs))) return coeffs, field @@ -1757,9 +1758,9 @@ def _convert_to_coeffs(a: Poly | Array | int, field: Type[Array]) -> Array: coeffs = a.coeffs elif isinstance(a, int): # Scalar multiplication - coeffs = np.atleast_1d(field(a % field.characteristic)) + coeffs = cast(Array, np.atleast_1d(field(a % field.characteristic))) else: - coeffs = np.atleast_1d(a) + coeffs = cast(Array, np.atleast_1d(a)) return coeffs @@ -1777,7 +1778,7 @@ def _convert_to_integer(a: Poly | Array | int, field: Type[Array]) -> int: return integer -def _convert_to_sparse_coeffs(a: Poly | Array | int, field: Type[Array]) -> tuple[np.ndarray, Array]: +def _convert_to_sparse_coeffs(a: Poly | Array | int, field: Type[Array]) -> tuple[npt.NDArray, Array]: """ Convert the polynomial or finite field scalar into its non-zero degrees and coefficients. """ @@ -1787,9 +1788,9 @@ def _convert_to_sparse_coeffs(a: Poly | Array | int, field: Type[Array]) -> tupl elif isinstance(a, int): # Scalar multiplication degrees = np.array([0]) - coeffs = np.atleast_1d(field(a % field.characteristic)) + coeffs = cast(Array, np.atleast_1d(field(a % field.characteristic))) else: degrees = np.array([0]) - coeffs = np.atleast_1d(a) + coeffs = cast(Array, np.atleast_1d(a)) return degrees, coeffs diff --git a/src/galois/_polys/_primitive.py b/src/galois/_polys/_primitive.py index 49e8470f0..d97fa038f 100644 --- a/src/galois/_polys/_primitive.py +++ b/src/galois/_polys/_primitive.py @@ -32,7 +32,7 @@ def is_primitive(f: Poly) -> bool: .. question:: Why is this a method and not a property? :collapsible: - This is a method to indicate it is a computationally-expensive task. + This is a method to indicate it is a computationally expensive task. Returns: `True` if the polynomial is primitive. @@ -42,7 +42,7 @@ def is_primitive(f: Poly) -> bool: Notes: A degree-$m$ polynomial $f(x)$ over $\mathrm{GF}(q)$ is *primitive* if it is - irreducible and $f(x)\ |\ (x^k - 1)$ for $k = q^m - 1$ and no $k$ less than + irreducible and $f(x) \mid (x^k - 1)$ for $k = q^m - 1$ and no $k$ less than $q^m - 1$. References: @@ -125,8 +125,8 @@ def primitive_poly( method: The search method for finding the primitive polynomial. - - `"min"` (default): Returns the lexicographically-first polynomial. - - `"max"`: Returns the lexicographically-last polynomial. + - `"min"` (default): Returns the lexicographically first polynomial. + - `"max"`: Returns the lexicographically last polynomial. - `"random"`: Returns a random polynomial. Returns: @@ -149,7 +149,7 @@ def primitive_poly( $\mathrm{GF}(q^m) = \{0, 1, \alpha, \alpha^2, \dots, \alpha^{q^m-2}\}$. Examples: - Find the lexicographically-first, lexicographically-last, and a random monic primitive polynomial. + Find the lexicographically first, lexicographically last, and a random monic primitive polynomial. .. ipython:: python @@ -157,20 +157,20 @@ def primitive_poly( galois.primitive_poly(7, 3, method="max") galois.primitive_poly(7, 3, method="random") - Find the lexicographically-first monic primitive polynomial with four terms. + Find the lexicographically first monic primitive polynomial with four terms. .. ipython:: python galois.primitive_poly(7, 3, terms=4) - Find the lexicographically-first monic irreducible polynomial with the minimum number of non-zero terms. + Find the lexicographically first monic irreducible polynomial with the minimum number of non-zero terms. .. ipython:: python galois.primitive_poly(7, 3, terms="min") - Notice :func:`~galois.primitive_poly` returns the lexicographically-first primitive polynomial but - :func:`~galois.conway_poly` returns the lexicographically-first primitive polynomial that is *consistent* + Notice :func:`~galois.primitive_poly` returns the lexicographically first primitive polynomial but + :func:`~galois.conway_poly` returns the lexicographically first primitive polynomial that is *consistent* with smaller Conway polynomials. This is sometimes the same polynomial. .. ipython:: python @@ -371,7 +371,7 @@ def matlab_primitive_poly(characteristic: int, degree: int) -> Poly: Poly.is_primitive, primitive_poly, conway_poly Notes: - This function returns the same result as Matlab's `gfprimdf(m, p)`. Matlab uses the lexicographically-first + This function returns the same result as Matlab's `gfprimdf(m, p)`. Matlab uses the lexicographically first primitive polynomial with minimum terms, which is equivalent to `galois.primitive_poly(p, m, terms="min")`. There are three notable exceptions, however: @@ -416,18 +416,18 @@ def matlab_primitive_poly(characteristic: int, degree: int) -> Poly: f"Argument 'degree' must be at least 1, not {degree}. There are no primitive polynomials with degree 0." ) - # Textbooks and Matlab use the lexicographically-first primitive polynomial with minimal terms for the default. + # Textbooks and Matlab use the lexicographically first primitive polynomial with minimal terms for the default. # 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. + # Not the lexicographically first of x^7 + x + 1. 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. + # Not the lexicographically first of x^14 + x^5 + x^3 + x + 1. 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. + # Not the lexicographically first of x^16 + x^5 + x^3 + x^2 + 1. return Poly.Degrees([16, 12, 3, 1, 0]) return primitive_poly(characteristic, degree) diff --git a/src/galois/_polys/_sparse.py b/src/galois/_polys/_sparse.py index 3ff409023..a8b2add10 100644 --- a/src/galois/_polys/_sparse.py +++ b/src/galois/_polys/_sparse.py @@ -5,16 +5,17 @@ from __future__ import annotations import numpy as np +import numpy.typing as npt from .._domains import Array def add( - a_degrees: np.ndarray, + a_degrees: npt.NDArray, a_coeffs: Array, - b_degrees: np.ndarray, + b_degrees: npt.NDArray, b_coeffs: Array, -) -> tuple[np.ndarray, Array]: +) -> tuple[npt.NDArray, Array]: """ c(x) = a(x) + b(x) """ @@ -28,9 +29,9 @@ def add( def negative( - a_degrees: np.ndarray, + a_degrees: npt.NDArray, a_coeffs: Array, -) -> tuple[np.ndarray, Array]: +) -> tuple[npt.NDArray, Array]: """ c(x) = -a(x) a(x) + -a(x) = 0 @@ -39,11 +40,11 @@ def negative( def subtract( - a_degrees: np.ndarray, + a_degrees: npt.NDArray, a_coeffs: Array, - b_degrees: np.ndarray, + b_degrees: npt.NDArray, b_coeffs: Array, -) -> tuple[np.ndarray, Array]: +) -> tuple[npt.NDArray, Array]: """ c(x) = a(x) - b(x) """ @@ -58,11 +59,11 @@ def subtract( def multiply( - a_degrees: np.ndarray, + a_degrees: npt.NDArray, a_coeffs: Array, - b_degrees: np.ndarray, + b_degrees: npt.NDArray, b_coeffs: Array, -) -> tuple[np.ndarray, Array]: +) -> tuple[npt.NDArray, Array]: """ c(x) = a(x) * b(x) c(x) = a(x) * b = a(x) + ... + a(x) diff --git a/src/galois/_prime.py b/src/galois/_prime.py index 0ab7f2d60..9b80ae73d 100644 --- a/src/galois/_prime.py +++ b/src/galois/_prime.py @@ -89,7 +89,7 @@ def primes(n: int) -> list[int]: p = (prime_idxs * 2 + 3).tolist() # Convert indices back to odd integers p.insert(0, 2) # Add the only even prime, 2 - # Replace the global primes lookup table with the newly-created, larger list + # Replace the global primes lookup table with the newly created, larger list PRIMES = p MAX_K = len(PRIMES) MAX_N = n @@ -97,7 +97,7 @@ def primes(n: int) -> list[int]: return p -# TODO: Don't build a large lookup table at import time. Instead, use the progressively-growing nature of PRIMES. +# TODO: Don't build a large lookup table at import time. Instead, use the progressively growing nature of PRIMES. # Generate a prime lookup table for efficient lookup in other algorithms PRIMES = primes(10_000_000) MAX_K = len(PRIMES) @@ -644,7 +644,7 @@ def legendre_symbol(a: int, p: int) -> int: .. math:: \bigg(\frac{a}{p}\bigg) = \begin{cases} - 0, & p\ |\ a + 0, & p \mid a 1, & a \in Q_p @@ -1278,7 +1278,7 @@ def f(x): @export def divisors(n: int) -> list[int]: r""" - Computes all positive integer divisors $d$ of the integer $n$ such that $d\ |\ n$. + Computes all positive integer divisors $d$ of the integer $n$ such that $d \mid n$. Arguments: n: An integer. diff --git a/src/galois/typing.py b/src/galois/typing.py index 26ea007f8..3e92f030f 100644 --- a/src/galois/typing.py +++ b/src/galois/typing.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Sequence, Union import numpy as np +import numpy.typing as npt # Obtain forward references if TYPE_CHECKING: @@ -54,7 +55,7 @@ # Ascending degrees GF("2 + 2x + x^2") -- :obj:`~galois.Array`: A previously-created scalar :obj:`~galois.Array` object. No coercion is necessary. +- :obj:`~galois.Array`: A previously created scalar :obj:`~galois.Array` object. No coercion is necessary. .. rubric:: Alias """ @@ -90,7 +91,7 @@ """ -ArrayLike = Union[IterableLike, np.ndarray, "Array"] +ArrayLike = Union[IterableLike, npt.NDArray, "Array"] """ A :obj:`~typing.Union` representing objects that can be coerced into a Galois field array. @@ -115,7 +116,7 @@ x = np.array([[17, 4], [148, 205]]); x GF(x) -- :obj:`~galois.Array`: A previously-created :obj:`~galois.Array` object. No coercion is necessary. +- :obj:`~galois.Array`: A previously created :obj:`~galois.Array` object. No coercion is necessary. .. rubric:: Alias """ @@ -225,7 +226,7 @@ galois.Poly([2, 0, 1], field=GF) galois.Poly(GF([2, 0, 1])) -- :obj:`~galois.Poly`: A previously-created :obj:`~galois.Poly` object. No coercion is necessary. +- :obj:`~galois.Poly`: A previously created :obj:`~galois.Poly` object. No coercion is necessary. .. rubric:: Alias """ diff --git a/tests/fields/test_classes.py b/tests/fields/test_classes.py index b2523ddd7..838517ec8 100644 --- a/tests/fields/test_classes.py +++ b/tests/fields/test_classes.py @@ -95,7 +95,7 @@ def test_cant_set_attribute(attribute): def test_is_primitive_poly(): """ - Verify the `is_primitive_poly` boolean is calculated correctly for fields constructed with explicitly-specified + Verify the `is_primitive_poly` boolean is calculated correctly for fields constructed with explicitly specified irreducible polynomials. """ # GF(2^m) with integer dtype diff --git a/tests/polys/luts/irreducible_polys_min.py b/tests/polys/luts/irreducible_polys_min.py index 454d9de59..ae541f5ef 100644 --- a/tests/polys/luts/irreducible_polys_min.py +++ b/tests/polys/luts/irreducible_polys_min.py @@ -1,5 +1,5 @@ """ -A module containing LUTs for lexicographically-first irreducible polynomials with minimal terms. +A module containing LUTs for lexicographically first irreducible polynomials with minimal terms. LUT items obtained by randomly picking degrees and checking the PDF,