Repository: mhostetter/galois

Issue:
Error in `minimal_poly()` with large fields
As discovered [here](https://github.com/mhostetter/galois/issues/185#issuecomment-969938384), `galois.minimal_poly()` fails for large extension fields.

Here's a reproducible example:

```python
In [1]: import galois

In [2]: poly = galois.Poly.Degrees([256, 241, 178, 121, 0]); poly
Out[2]: Poly(x^256 + x^241 + x^178 + x^121 + 1, GF(2))

In [3]: GF = galois.GF((2**256), irreducible_poly=poly, primitive_element=2, verify=False)

In [4]: print(GF.properties)
GF(2^256):
  characteristic: 2
  degree: 256
  order: 115792089237316195423570985008687907853269984665640564039457584007913129639936
  irreducible_poly: x^256 + x^241 + x^178 + x^121 + 1
  is_primitive_poly: True
  primitive_element: x

In [5]: x = GF(4); x
Out[5]: GF(4, order=2^256)

In [6]: galois.minimal_poly(x)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-976c7185bfb2> in <module>
----> 1 galois.minimal_poly(x)

/mnt/c/Users/matth/repos/galois/galois/_polys/_functions.py in minimal_poly(element)
    116         conjugates = np.unique(element**(field.characteristic**np.arange(0, field.degree)))
    117         poly = Poly.Roots(conjugates, field=field)
--> 118         poly = Poly(poly.coeffs, field=field.prime_subfield)
    119         return poly

/mnt/c/Users/matth/repos/galois/galois/_polys/_poly.py in __new__(cls, coeffs, field, order)
    109             coeffs = coeffs[::-1]  # Ensure it's in descending-degree order
    110
--> 111         coeffs, field = cls._convert_coeffs(coeffs, field)
    112
    113         if field is GF2:

/mnt/c/Users/matth/repos/galois/galois/_polys/_poly.py in _convert_coeffs(cls, coeffs, field)
    136                 # Ensure coeffs is an iterable
    137                 coeffs = coeffs.tolist()
--> 138             coeffs = field([int(-field(abs(c))) if c < 0 else c for c in coeffs])
    139
    140         return coeffs, field

/mnt/c/Users/matth/repos/galois/galois/_fields/_array.py in __new__(cls, array, dtype, copy, order, ndmin)
    116         if cls is FieldArray:
    117             raise NotImplementedError("FieldArray is an abstract base class that cannot be directly instantiated. Instead, create a FieldArray subclass for GF(p^m) arithmetic using `GF = galois.GF(p**m)` and instantiate an array using `x = GF(array_like)`.")
--> 118         return cls._array(array, dtype=dtype, copy=copy, order=order, ndmin=ndmin)
    119
    120     def __init__(self, array, dtype=None, copy=True, order="K", ndmin=0):

/mnt/c/Users/matth/repos/galois/galois/_fields/_array.py in _array(cls, array_like, dtype, copy, order, ndmin)
    171     def _array(cls, array_like, dtype=None, copy=True, order="K", ndmin=0):
    172         dtype = cls._get_dtype(dtype)
--> 173         array_like = cls._check_array_like_object(array_like)
    174         array = np.array(array_like, dtype=dtype, copy=copy, order=order, ndmin=ndmin)
    175         return array.view(cls)

/mnt/c/Users/matth/repos/galois/galois/_fields/_array.py in _check_array_like_object(cls, array_like)
    191             # Recursively check the items in the iterable to ensure they're of the correct type
    192             # and that their values are in range
--> 193             array_like = cls._check_iterable_types_and_values(array_like)
    194         elif isinstance(array_like, np.ndarray):
    195             # If this a NumPy array, but not a FieldArray, verify the array

/mnt/c/Users/matth/repos/galois/galois/_fields/_array.py in _check_iterable_types_and_values(cls, iterable)
    218                 raise TypeError(f"When {cls.name} arrays are created/assigned with an iterable, each element must be an integer. Found type {type(item)}.")
    219
--> 220             cls._check_array_values(item)
    221             # if not 0 <= item < cls.order:
    222             #     raise ValueError(f"{cls.name} arrays must have elements in 0 <= x < {cls.order}, not {item}.")

/mnt/c/Users/matth/repos/galois/galois/_fields/_array.py in _check_array_values(cls, array)
    257             idxs = np.logical_or(array < 0, array >= cls.order)
    258             values = array if array.ndim == 0 else array[idxs]
--> 259             raise ValueError(f"{cls.name} arrays must have elements in `0 <= x < {cls.order}`, not {values}.")
    260
    261     @classmethod

ValueError: GF(2) arrays must have elements in `0 <= x < 2`, not 107745580774404846491629807443664560507035291931888949065819222618122380761054.
```

After this PR, the example runs as follows:

```python
In [1]: import galois

In [2]: poly = galois.Poly.Degrees([256, 241, 178, 121, 0]); poly
Out[2]: Poly(x^256 + x^241 + x^178 + x^121 + 1, GF(2))

In [3]: GF = galois.GF((2**256), irreducible_poly=poly, primitive_element=2, verify=False)

In [4]: print(GF.properties)
GF(2^256):
  characteristic: 2
  degree: 256
  order: 115792089237316195423570985008687907853269984665640564039457584007913129639936
  irreducible_poly: x^256 + x^241 + x^178 + x^121 + 1
  is_primitive_poly: True
  primitive_element: x

In [5]: x = GF(4); x
Out[5]: GF(4, order=2^256)

In [6]: galois.minimal_poly(x)
Out[6]: Poly(x^256 + x^241 + x^178 + x^121 + 1, GF(2))
```

Comments:

Patch:
diff --git a/galois/_codes/_bch.py b/galois/_codes/_bch.py
index 2278eae7e..48c95131c 100644
--- a/galois/_codes/_bch.py
+++ b/galois/_codes/_bch.py
@@ -89,7 +89,7 @@ def bch_valid_codes(n, t_min=1):
     while True:
         c = 1
         roots = alpha**(c + np.arange(0, 2*t))
-        powers = GF.characteristic**np.arange(0, GF.degree)
+        powers = GF.characteristic**np.arange(0, GF.degree, dtype=GF.dtypes[-1])
         conjugates = np.unique(np.power.outer(roots, powers))
         g_degree = len(conjugates)
         k = n - g_degree
@@ -201,7 +201,7 @@ class BCH:
             # minimal polynomial and then doing an LCM, we will compute all the unique conjugates of all the roots
             # and then compute (x - c1)*(x - c2)*...*(x - cn), which is equivalent.
             roots = alpha**(c + np.arange(0, 2*t))
-            powers = GF.characteristic**np.arange(0, GF.degree)
+            powers = GF.characteristic**np.arange(0, GF.degree, dtype=GF.dtypes[-1])
             conjugates = np.unique(np.power.outer(roots, powers))
             g_degree = len(conjugates)
 
diff --git a/galois/_codes/_cyclic.py b/galois/_codes/_cyclic.py
index 289b5527a..19da32611 100644
--- a/galois/_codes/_cyclic.py
+++ b/galois/_codes/_cyclic.py
@@ -109,6 +109,7 @@ def roots_to_parity_check_matrix(n, roots):
     if not isinstance(roots, FieldArray):
         raise TypeError(f"Argument `roots` must be a galois.FieldArray, not {type(roots)}.")
 
-    H = np.power.outer(roots, np.arange(n - 1, -1, -1))
+    GF = type(roots)
+    H = np.power.outer(roots, np.arange(n - 1, -1, -1, dtype=GF.dtypes[-1]))
 
     return H
diff --git a/galois/_fields/_array.py b/galois/_fields/_array.py
index dcfc80c2b..d7554ea94 100644
--- a/galois/_fields/_array.py
+++ b/galois/_fields/_array.py
@@ -765,7 +765,7 @@ class FieldArray(np.ndarray, metaclass=FieldClass):
         subfield = field.prime_subfield
         p = field.characteristic
         m = field.degree
-        conjugates = np.power.outer(self, p**np.arange(0, m, dtype=self.dtype))
+        conjugates = np.power.outer(self, p**np.arange(0, m, dtype=field.dtypes[-1]))
         trace = np.add.reduce(conjugates, axis=-1)
         return subfield(trace)
 
diff --git a/galois/_fields/_class.py b/galois/_fields/_class.py
index fe72b56fb..746214926 100644
--- a/galois/_fields/_class.py
+++ b/galois/_fields/_class.py
@@ -313,7 +313,7 @@ class FieldClass(FunctionMeta, UfuncMeta, PropertiesMeta):
 
         if cls.display_mode == "power":
             # Order elements by powers of the primitive element
-            x_default = np.concatenate((np.atleast_1d(cls(0)), cls.primitive_element**np.arange(0, cls.order - 1)))
+            x_default = np.concatenate((np.atleast_1d(cls(0)), cls.primitive_element**np.arange(0, cls.order - 1, dtype=cls.dtypes[-1])))
         else:
             x_default = cls.Elements()
         y_default = x_default if operation != "/" else x_default[1:]
diff --git a/galois/_polys/_functions.py b/galois/_polys/_functions.py
index 8fd3d83de..34a50c27b 100644
--- a/galois/_polys/_functions.py
+++ b/galois/_polys/_functions.py
@@ -113,7 +113,7 @@ def minimal_poly(element):
     if field.is_prime_field:
         return x - element
     else:
-        conjugates = np.unique(element**(field.characteristic**np.arange(0, field.degree)))
+        conjugates = np.unique(element**(field.characteristic**np.arange(0, field.degree, dtype=field.dtypes[-1])))
         poly = Poly.Roots(conjugates, field=field)
         poly = Poly(poly.coeffs, field=field.prime_subfield)
         return poly


Test patch:
diff --git a/tests/polys/test_minimal_polys.py b/tests/polys/test_minimal_polys.py
index 814d5507f..daec41c4c 100644
--- a/tests/polys/test_minimal_polys.py
+++ b/tests/polys/test_minimal_polys.py
@@ -380,3 +380,11 @@ def test_minimal_poly(characteristic, degree):
         e = GFpm(item[0])
         poly = galois.Poly(item[1], field=GFp)
         assert galois.minimal_poly(e) == poly
+
+
+def test_minimal_poly_large_field():
+    # Test vectors generated with SageMath
+    GF = galois.GF(2**100)
+    galois.minimal_poly(GF(2)) == galois.Poly.String("x^100 + x^57 + x^56 + x^55 + x^52 + x^48 + x^47 + x^46 + x^45 + x^44 + x^43 + x^41 + x^37 + x^36 + x^35 + x^34 + x^31 + x^30 + x^27 + x^25 + x^24 + x^22 + x^20 + x^19 + x^16 + x^15 + x^11 + x^9 + x^8 + x^6 + x^5 + x^3 + 1")
+    galois.minimal_poly(GF(3)) == galois.Poly.String("x^100 + x^96 + x^68 + x^64 + x^57 + x^55 + x^54 + x^53 + x^51 + x^50 + x^48 + x^47 + x^42 + x^41 + x^38 + x^36 + x^31 + x^29 + x^26 + x^24 + x^15 + x^14 + x^12 + x^9 + x^8 + x^5 + x^2 + x + 1")
+    galois.minimal_poly(GF(6)) == galois.Poly.String("x^100 + x^78 + x^76 + x^74 + x^73 + x^71 + x^67 + x^66 + x^65 + x^62 + x^60 + x^55 + x^52 + x^51 + x^50 + x^48 + x^47 + x^45 + x^42 + x^41 + x^35 + x^34 + x^33 + x^31 + x^30 + x^29 + x^28 + x^27 + x^26 + x^23 + x^22 + x^21 + x^20 + x^19 + x^16 + x^14 + x^13 + x^12 + x^10 + x^9 + x^8 + x^6 + x^3 + x + 1")