Repository: mhostetter/galois

Issue:
Better item assignment for "large" fields
I may be able to remove the requirement to cast elements to ints before arithmetic. This is required because "large" fields, which use `dtype=object` and Python ints, can get `FieldArray` objects in their arrays through assignment. For example, `x[0] = GF(10)` would assign a 0-D `FieldArray` scalar to index 0, *not* the integer 10.

With a smarter implementation of `__setitem__()`, I may be able to move the integer casting logic there and improve arithmetic performance.

See #253.

```python
    @staticmethod
    @numba.extending.register_jitable
    def _multiply_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
        """
        a in GF(2^m), can be represented as a degree m-1 polynomial a(x) in GF(2)[x]
        b in GF(2^m), can be represented as a degree m-1 polynomial b(x) in GF(2)[x]
        p(x) in GF(2)[x] with degree m is the irreducible polynomial of GF(2^m)

        a * b = c
            = (a(x) * b(x)) % p(x) in GF(2)
            = c(x)
            = c
        """
        ORDER = CHARACTERISTIC**DEGREE
        a = int(a)          # Remove this!
        b = int(b)          # Remove this!

        # Re-order operands such that a > b so the while loop has less loops
        if b > a:
            a, b = b, a

        c = 0
        while b > 0:
            if b & 0b1:
                c ^= a  # Add a(x) to c(x)

            b >>= 1  # Divide b(x) by x
            a <<= 1  # Multiply a(x) by x
            if a >= ORDER:
                a ^= IRREDUCIBLE_POLY  # Compute a(x) % p(x)

        return c
```

Discussion:

Patch:
diff --git a/galois/_fields/_gf2m.py b/galois/_fields/_gf2m.py
index 299b13362..833b2e3d6 100644
--- a/galois/_fields/_gf2m.py
+++ b/galois/_fields/_gf2m.py
@@ -58,13 +58,6 @@ class GF2mMeta(FieldClass, DirMeta):
 
     ###############################################################################
     # Arithmetic functions using explicit calculation
-    #
-    # NOTE: The ufunc inputs a and b are cast to integers at the beginning of each
-    #       ufunc to prevent the non-JIT-compiled invocations (used in "large"
-    #       fields with dtype=object) from performing infintely recursive
-    #       arithmetic. Instead, the intended arithmetic inside the ufuncs is
-    #       integer arithmetic.
-    #       See https://github.com/mhostetter/galois/issues/253.
     ###############################################################################
 
     @staticmethod
@@ -72,9 +65,6 @@ class GF2mMeta(FieldClass, DirMeta):
         """
         Not actually used. `np.bitwise_xor()` is faster.
         """
-        a = int(a)
-        b = int(b)
-
         return a ^ b
 
     @staticmethod
@@ -82,8 +72,6 @@ class GF2mMeta(FieldClass, DirMeta):
         """
         Not actually used. `np.positive()` is faster.
         """
-        a = int(a)
-
         return a
 
     @staticmethod
@@ -91,9 +79,6 @@ class GF2mMeta(FieldClass, DirMeta):
         """
         Not actually used. `np.bitwise_xor()` is faster.
         """
-        a = int(a)
-        b = int(b)
-
         return a ^ b
 
     @staticmethod
@@ -105,13 +90,11 @@ class GF2mMeta(FieldClass, DirMeta):
         p(x) in GF(2)[x] with degree m is the irreducible polynomial of GF(2^m)
 
         a * b = c
-            = (a(x) * b(x)) % p(x) in GF(2)
-            = c(x)
-            = c
+              = (a(x) * b(x)) % p(x) in GF(2)
+              = c(x)
+              = c
         """
         ORDER = CHARACTERISTIC**DEGREE
-        a = int(a)
-        b = int(b)
 
         # Re-order operands such that a > b so the while loop has less loops
         if b > a:
@@ -145,7 +128,6 @@ class GF2mMeta(FieldClass, DirMeta):
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
         ORDER = CHARACTERISTIC**DEGREE
-        a = int(a)
 
         exponent = ORDER - 2
         result_s = a  # The "squaring" part
@@ -169,9 +151,6 @@ class GF2mMeta(FieldClass, DirMeta):
         if b == 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if a == 0:
             c = 0
         else:
@@ -197,9 +176,6 @@ class GF2mMeta(FieldClass, DirMeta):
         if a == 0 and b < 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if b == 0:
             return 1
         elif b < 0:
@@ -236,8 +212,6 @@ class GF2mMeta(FieldClass, DirMeta):
             raise ArithmeticError("Cannot compute the discrete logarithm of 0 in a Galois field.")
 
         ORDER = CHARACTERISTIC**DEGREE
-        a = int(a)
-        b = int(b)
 
         # Naive algorithm
         result = 1
diff --git a/galois/_fields/_gfp.py b/galois/_fields/_gfp.py
index 0638a4aa8..d41238e74 100644
--- a/galois/_fields/_gfp.py
+++ b/galois/_fields/_gfp.py
@@ -56,21 +56,11 @@ class GFpMeta(FieldClass, DirMeta):
 
     ###############################################################################
     # Arithmetic functions using explicit calculation
-    #
-    # NOTE: The ufunc inputs a and b are cast to integers at the beginning of each
-    #       ufunc to prevent the non-JIT-compiled invocations (used in "large"
-    #       fields with dtype=object) from performing infintely recursive
-    #       arithmetic. Instead, the intended arithmetic inside the ufuncs is
-    #       integer arithmetic.
-    #       See https://github.com/mhostetter/galois/issues/253.
     ###############################################################################
 
     @staticmethod
     @numba.extending.register_jitable
     def _add_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-        b = int(b)
-
         c = a + b
         if c >= CHARACTERISTIC:
             c -= CHARACTERISTIC
@@ -80,8 +70,6 @@ class GFpMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _negative_calculate(a, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-
         if a == 0:
             c = 0
         else:
@@ -92,9 +80,6 @@ class GFpMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _subtract_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-        b = int(b)
-
         if a >= b:
             c = a - b
         else:
@@ -105,9 +90,6 @@ class GFpMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _multiply_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-        b = int(b)
-
         c = (a * b) % CHARACTERISTIC
 
         return c
@@ -124,8 +106,6 @@ class GFpMeta(FieldClass, DirMeta):
         if a == 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-
         r2, r1 = CHARACTERISTIC, a
         t2, t1 = 0, 1
 
@@ -145,9 +125,6 @@ class GFpMeta(FieldClass, DirMeta):
         if b == 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if a == 0:
             c = 0
         else:
@@ -163,19 +140,16 @@ class GFpMeta(FieldClass, DirMeta):
         Square and Multiply Algorithm
 
         a^13 = (1) * (a)^13
-            = (a) * (a)^12
-            = (a) * (a^2)^6
-            = (a) * (a^4)^3
-            = (a * a^4) * (a^4)^2
-            = (a * a^4) * (a^8)
-            = result_m * result_s
+             = (a) * (a)^12
+             = (a) * (a^2)^6
+             = (a) * (a^4)^3
+             = (a * a^4) * (a^4)^2
+             = (a * a^4) * (a^8)
+             = result_m * result_s
         """
         if a == 0 and b < 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if b == 0:
             return 1
         elif b < 0:
@@ -212,8 +186,6 @@ class GFpMeta(FieldClass, DirMeta):
             raise ArithmeticError("Cannot compute the discrete logarithm of 0 in a Galois field.")
 
         ORDER = CHARACTERISTIC**DEGREE
-        a = int(a)
-        b = int(b)
 
         # Naive algorithm
         result = 1
diff --git a/galois/_fields/_gfpm.py b/galois/_fields/_gfpm.py
index 207d8f905..ca772fae9 100644
--- a/galois/_fields/_gfpm.py
+++ b/galois/_fields/_gfpm.py
@@ -154,13 +154,6 @@ class GFpmMeta(FieldClass, DirMeta):
 
     ###############################################################################
     # Arithmetic functions using explicit calculation
-    #
-    # NOTE: The ufunc inputs a and b are cast to integers at the beginning of each
-    #       ufunc to prevent the non-JIT-compiled invocations (used in "large"
-    #       fields with dtype=object) from performing infintely recursive
-    #       arithmetic. Instead, the intended arithmetic inside the ufuncs is
-    #       integer arithmetic.
-    #       See https://github.com/mhostetter/galois/issues/253.
     ###############################################################################
 
     @staticmethod
@@ -169,8 +162,6 @@ class GFpmMeta(FieldClass, DirMeta):
         """
         Convert the integer representation to vector/polynomial representation
         """
-        a = int(a)
-
         a_vec = np.zeros(DEGREE, dtype=DTYPE)
         for i in range(DEGREE - 1, -1, -1):
             q, r = divmod(a, CHARACTERISTIC)
@@ -196,9 +187,6 @@ class GFpmMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _add_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-        b = int(b)
-
         a_vec = INT_TO_POLY(a, CHARACTERISTIC, DEGREE)
         b_vec = INT_TO_POLY(b, CHARACTERISTIC, DEGREE)
         c_vec = (a_vec + b_vec) % CHARACTERISTIC
@@ -209,8 +197,6 @@ class GFpmMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _negative_calculate(a, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-
         a_vec = INT_TO_POLY(a, CHARACTERISTIC, DEGREE)
         a_vec = (-a_vec) % CHARACTERISTIC
         c = POLY_TO_INT(a_vec, CHARACTERISTIC, DEGREE)
@@ -220,9 +206,6 @@ class GFpmMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _subtract_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-        b = int(b)
-
         a_vec = INT_TO_POLY(a, CHARACTERISTIC, DEGREE)
         b_vec = INT_TO_POLY(b, CHARACTERISTIC, DEGREE)
         c_vec = (a_vec - b_vec) % CHARACTERISTIC
@@ -233,9 +216,6 @@ class GFpmMeta(FieldClass, DirMeta):
     @staticmethod
     @numba.extending.register_jitable
     def _multiply_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
-        a = int(a)
-        b = int(b)
-
         a_vec = INT_TO_POLY(a, CHARACTERISTIC, DEGREE)
         b_vec = INT_TO_POLY(b, CHARACTERISTIC, DEGREE)
 
@@ -279,7 +259,6 @@ class GFpmMeta(FieldClass, DirMeta):
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
         ORDER = CHARACTERISTIC**DEGREE
-        a = int(a)
 
         exponent = ORDER - 2
         result_s = a  # The "squaring" part
@@ -303,9 +282,6 @@ class GFpmMeta(FieldClass, DirMeta):
         if b == 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if a == 0:
             c = 0
         else:
@@ -321,19 +297,16 @@ class GFpmMeta(FieldClass, DirMeta):
         Square and Multiply Algorithm
 
         a^13 = (1) * (a)^13
-            = (a) * (a)^12
-            = (a) * (a^2)^6
-            = (a) * (a^4)^3
-            = (a * a^4) * (a^4)^2
-            = (a * a^4) * (a^8)
-            = result_m * result_s
+             = (a) * (a)^12
+             = (a) * (a^2)^6
+             = (a) * (a^4)^3
+             = (a * a^4) * (a^4)^2
+             = (a * a^4) * (a^8)
+             = result_m * result_s
         """
         if a == 0 and b < 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if b == 0:
             return 1
         elif b < 0:
@@ -370,8 +343,6 @@ class GFpmMeta(FieldClass, DirMeta):
             raise ArithmeticError("Cannot compute the discrete logarithm of 0 in a Galois field.")
 
         ORDER = CHARACTERISTIC**DEGREE
-        a = int(a)
-        b = int(b)
 
         # Naive algorithm
         result = 1
diff --git a/galois/_fields/_main.py b/galois/_fields/_main.py
index bf5b187e7..d08808c02 100644
--- a/galois/_fields/_main.py
+++ b/galois/_fields/_main.py
@@ -1171,7 +1171,10 @@ class FieldArray(np.ndarray, metaclass=FieldClass):
             cls._verify_scalar_value(array_like)
         elif isinstance(array_like, cls):
             # This was a previously-created and vetted array -- there's no need to re-verify
-            pass
+            if array_like.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.
+                array_like = int(array_like)
         elif isinstance(array_like, str):
             array_like = cls._convert_to_element(array_like)
             cls._verify_scalar_value(array_like)
@@ -2731,13 +2734,6 @@ class GF2Meta(FieldClass, DirMeta):
 
     ###############################################################################
     # Arithmetic functions using explicit calculation
-    #
-    # NOTE: The ufunc inputs a and b are cast to integers at the beginning of each
-    #       ufunc to prevent the non-JIT-compiled invocations (used in "large"
-    #       fields with dtype=object) from performing infintely recursive
-    #       arithmetic. Instead, the intended arithmetic inside the ufuncs is
-    #       integer arithmetic.
-    #       See https://github.com/mhostetter/galois/issues/253.
     ###############################################################################
 
     @staticmethod
@@ -2745,9 +2741,6 @@ class GF2Meta(FieldClass, DirMeta):
         """
         Not actually used. `np.bitwise_xor()` is faster.
         """
-        a = int(a)
-        b = int(b)
-
         return a ^ b
 
     @staticmethod
@@ -2755,8 +2748,6 @@ class GF2Meta(FieldClass, DirMeta):
         """
         Not actually used. `np.positive()` is faster.
         """
-        a = int(a)
-
         return a
 
     @staticmethod
@@ -2764,9 +2755,6 @@ class GF2Meta(FieldClass, DirMeta):
         """
         Not actually used. `np.bitwise_xor()` is faster.
         """
-        a = int(a)
-        b = int(b)
-
         return a ^ b
 
     @staticmethod
@@ -2774,9 +2762,6 @@ class GF2Meta(FieldClass, DirMeta):
         """
         Not actually used. `np.bitwise_and()` is faster.
         """
-        a = int(a)
-        b = int(b)
-
         return a & b
 
     @staticmethod
@@ -2791,9 +2776,6 @@ class GF2Meta(FieldClass, DirMeta):
         if b == 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         return a & b
 
     @staticmethod
@@ -2802,9 +2784,6 @@ class GF2Meta(FieldClass, DirMeta):
         if a == 0 and b < 0:
             raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")
 
-        a = int(a)
-        b = int(b)
-
         if b == 0:
             return 1
 


Test patch:
diff --git a/tests/fields/test_assignment.py b/tests/fields/test_assignment.py
index aee3d3ad0..5124eb208 100644
--- a/tests/fields/test_assignment.py
+++ b/tests/fields/test_assignment.py
@@ -22,6 +22,14 @@ class TestConstantIndex:
         with pytest.raises(ValueError):
             a[0] = field.order
 
+    def test_always_int_object(self):
+        # Ensure when assigning FieldArray elements to an array they are converted to ints
+        GF = galois.GF(2**100)
+        a = GF.Random(10)
+        assert np.all(is_int(a))
+        a[0] = GF(10)
+        assert np.all(is_int(a))
+
 
 class TestSliceIndex:
     def test_constant_valid(self, field):
@@ -70,6 +78,14 @@ class TestSliceIndex:
         with pytest.raises(ValueError):
             a[0:2] = np.array([field.order, 1])
 
+    def test_always_int_object(self):
+        # Ensure when assigning FieldArray elements to an array they are converted to ints
+        GF = galois.GF(2**100)
+        a = GF.Random(10)
+        assert np.all(is_int(a))
+        a[0:3] = [GF(10), GF(20), GF(30)]
+        assert np.all(is_int(a))
+
 
 class Test2DSliceIndex:
     def test_list_valid(self, field):
@@ -103,3 +119,14 @@ class Test2DSliceIndex:
         a = field.Random((10,10))
         with pytest.raises(ValueError):
             a[0:2, 0:2] = np.array([[field.order, 1], [1, 1]])
+
+    def test_always_int_object(self):
+        # Ensure when assigning FieldArray elements to an array they are converted to ints
+        GF = galois.GF(2**100)
+        a = GF.Random((10,10))
+        assert np.all(is_int(a))
+        a[0:2, 0:2] = [[GF(10), GF(20)], [GF(30), GF(40)]]
+        assert np.all(is_int(a))
+
+
+is_int = np.vectorize(lambda element: isinstance(element, int))