Skip to content

Commit

Permalink
Support polynomial scalar multiplication
Browse files Browse the repository at this point in the history
Fixes #245
  • Loading branch information
mhostetter committed Jan 28, 2022
1 parent 0a37672 commit 70dc6e2
Show file tree
Hide file tree
Showing 69 changed files with 189 additions and 55 deletions.
165 changes: 119 additions & 46 deletions galois/_fields/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3778,30 +3778,54 @@ def __len__(self) -> int:
return self.degree + 1

def _check_inputs_are_polys(self, a, b):
"""
Verify polynomial arithmetic operands are either galois.Poly or scalars in a finite field.
"""
if not isinstance(a, (Poly, self.field)):
raise TypeError(f"Both operands must be a galois.Poly or a single element of its field {b.field.name}, not {type(a)}.")
raise TypeError(f"Both operands must be a galois.Poly or a single element of its field {self.field.name}, not {type(a)}.")
if not isinstance(b, (Poly, self.field)):
raise TypeError(f"Both operands must be a galois.Poly or a single element of its field {a.field.name}, not {type(b)}.")
raise TypeError(f"Both operands must be a galois.Poly or a single element of its field {self.field.name}, not {type(b)}.")
if (isinstance(a, Poly) and isinstance(b, Poly)) and not a.field is b.field:
raise TypeError(f"Both polynomial operands must be over the same field, not {a.field.name} and {b.field.name}.")

def _check_inputs_are_polys_or_ints(self, a, b):
"""
Verify polynomial arithmetic operands are either galois.Poly, scalars in a finite field, or an integer (scalar multiplication).
"""
if not isinstance(a, (Poly, self.field, int, np.integer)):
raise TypeError(f"Both operands must be a galois.Poly, a single element of its field {self.field.name}, or an integer, not {type(a)}.")
if not isinstance(b, (Poly, self.field, int, np.integer)):
raise TypeError(f"Both operands must be a galois.Poly, a single element of its field {self.field.name}, or an integer, not {type(b)}.")
if (isinstance(a, Poly) and isinstance(b, Poly)) and not a.field is b.field:
raise TypeError(f"Both polynomial operands must be over the same field, not {a.field.name} and {b.field.name}.")

def _convert_field_scalars_to_polys(self, a, b):
"""
Convert finite field scalars to 0-degree polynomials in that field.
"""
# Promote a single field element to a 0-degree polynomial
if not isinstance(a, Poly):
if isinstance(a, self.field):
if not a.size == 1:
raise ValueError(f"Arguments that are Galois field elements must have size 1 (equivalently a 0-degree polynomial), not size {a.size}.")
a = Poly(np.atleast_1d(a))
if not isinstance(b, Poly):
if isinstance(b, self.field):
if not b.size == 1:
raise ValueError(f"Arguments that are Galois field elements must have size 1 (equivalently a 0-degree polynomial), not size {b.size}.")
b = Poly(np.atleast_1d(b))

if not a.field is b.field:
raise TypeError(f"Both polynomial operands must be over the same field, not {str(a.field)} and {str(b.field)}.")
return a, b

@staticmethod
def _determine_poly_class(a, b):
"""
Determine the type of polynomial arithmetic to perform.
"""
if isinstance(a, SparsePoly) or isinstance(b, SparsePoly):
return SparsePoly, a, b
return SparsePoly
elif isinstance(a, BinaryPoly) or isinstance(b, BinaryPoly):
return BinaryPoly, a, b
return BinaryPoly
else:
return DensePoly, a, b
return DensePoly

def __add__(self, other):
"""
Expand All @@ -3825,11 +3849,15 @@ def __add__(self, other):
b = galois.Poly.Random(3); b
a + b
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._add(a, b)

def __radd__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._add(b, a)

def __sub__(self, other):
Expand All @@ -3854,11 +3882,15 @@ def __sub__(self, other):
b = galois.Poly.Random(3); b
a - b
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._sub(a, b)

def __rsub__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._sub(b, a)

def __mul__(self, other):
Expand All @@ -3883,11 +3915,21 @@ def __mul__(self, other):
b = galois.Poly.Random(3); b
a * b
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys_or_ints(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
if isinstance(a, (int, np.integer)):
# Ensure the integer is in the second operand for scalar multiplication
a, b = b, a
cls = self._determine_poly_class(a, b)
return cls._mul(a, b)

def __rmul__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys_or_ints(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
if isinstance(b, (int, np.integer)):
# Ensure the integer is in the second operand for scalar multiplication
b, a = a, b
cls = self._determine_poly_class(a, b)
return cls._mul(b, a)

def __divmod__(self, other):
Expand Down Expand Up @@ -3916,11 +3958,15 @@ def __divmod__(self, other):
q, r
b*q + r
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._divmod(a, b)

def __rdivmod__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._divmod(b, a)

def __truediv__(self, other):
Expand Down Expand Up @@ -3948,11 +3994,15 @@ def __truediv__(self, other):
divmod(a, b)
a / b
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._divmod(a, b)[0]

def __rtruediv__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._divmod(b, a)[0]

def __floordiv__(self, other):
Expand Down Expand Up @@ -3980,11 +4030,15 @@ def __floordiv__(self, other):
divmod(a, b)
a // b
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._divmod(a, b)[0]

def __rfloordiv__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._divmod(b, a)[0]

def __mod__(self, other):
Expand All @@ -4010,11 +4064,15 @@ def __mod__(self, other):
divmod(a, b)
a % b
"""
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._mod(a, b)

def __rmod__(self, other):
cls, a, b = self._check_inputs_are_polys(self, other)
self._check_inputs_are_polys(self, other)
a, b = self._convert_field_scalars_to_polys(self, other)
cls = self._determine_poly_class(a, b)
return cls._mod(b, a)

def __pow__(self, other):
Expand Down Expand Up @@ -4043,7 +4101,8 @@ def __pow__(self, other):
raise TypeError(f"For polynomial exponentiation, the second argument must be an int, not {other}.")
if not other >= 0:
raise ValueError(f"Can only exponentiate polynomials to non-negative integers, not {other}.")
field, a, power = self.field, self, other
a, power = self, other
field = self.field

# c(x) = a(x) ** power
if power == 0:
Expand Down Expand Up @@ -4074,11 +4133,11 @@ def __eq__(self, other):
elif isinstance(other, FieldArray):
# Compare poly to a finite field scalar (may or may not be from the same field)
if not other.ndim == 0:
raise ValueError(f"Can only compare Poly to a 0-D FieldArray scalar, not shape {other.shape}.")
raise ValueError(f"Can only compare galois.Poly to a 0-D galois.FieldArray scalar, not shape {other.shape}.")
return self.field is type(other) and self.degree == 0 and np.array_equal(self.coeffs, np.atleast_1d(other))

elif not isinstance(other, Poly):
raise TypeError(f"Can only compare Poly and Poly / int / FieldArray scalar objects, not {type(other)}.")
raise TypeError(f"Can only compare galois.Poly and galois.Poly / int / galois.FieldArray scalar objects, not {type(other)}.")

else:
# Compare two poly objects to each other
Expand Down Expand Up @@ -4310,8 +4369,12 @@ def _sub(cls, a, b):

@classmethod
def _mul(cls, a, b):
# c(x) = a(x) * b(x)
c_coeffs = np.convolve(a.coeffs, b.coeffs)
if isinstance(b, (int, np.integer)):
# Scalar multiplication (p * 3 = p + p + p)
c_coeffs = a.coeffs * b
else:
# c(x) = a(x) * b(x)
c_coeffs = np.convolve(a.coeffs, b.coeffs)

return Poly(c_coeffs)

Expand Down Expand Up @@ -4410,20 +4473,25 @@ def _sub(cls, a, b):

@classmethod
def _mul(cls, a, b):
# Re-order operands such that a > b so the while loop has less loops
a = a.integer
b = b.integer
if b > a:
a, b = b, a
if isinstance(b, (int, np.integer)):
# Scalar multiplication (p * 3 = p + p + p)
return BinaryPoly(a.integer) if b % 2 == 1 else BinaryPoly(0)

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
else:
# Re-order operands such that a > b so the while loop has less loops
a = a.integer
b = b.integer
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

return BinaryPoly(c)
return BinaryPoly(c)

@classmethod
def _divmod(cls, a, b):
Expand Down Expand Up @@ -4578,13 +4646,18 @@ def _sub(cls, a, b):
def _mul(cls, a, b):
field = a.field

# c(x) = a(x) * b(x)
cc = {}
for a_degree, a_coeff in zip(a.nonzero_degrees, a.nonzero_coeffs):
for b_degree, b_coeff in zip(b.nonzero_degrees, b.nonzero_coeffs):
cc[a_degree + b_degree] = cc.get(a_degree + b_degree, field(0)) + a_coeff*b_coeff
if isinstance(b, (int, np.integer)):
# Scalar multiplication (p * 3 = p + p + p)
return Poly.Degrees(a.nonzero_degrees, a.nonzero_coeffs * b)

return Poly.Degrees(list(cc.keys()), list(cc.values()), field=field)
else:
# c(x) = a(x) * b(x)
cc = {}
for a_degree, a_coeff in zip(a.nonzero_degrees, a.nonzero_coeffs):
for b_degree, b_coeff in zip(b.nonzero_degrees, b.nonzero_coeffs):
cc[a_degree + b_degree] = cc.get(a_degree + b_degree, field(0)) + a_coeff*b_coeff

return Poly.Degrees(list(cc.keys()), list(cc.values()), field=field)

@classmethod
def _divmod(cls, a, b):
Expand Down
14 changes: 14 additions & 0 deletions scripts/generate_field_test_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def make_luts(field, folder, sparse=False):
os.mkdir(folder)

FIELD = field
characteristic = int(field.characteristic())
order = int(field.order())
dtype = np.int64 if order <= np.iinfo(np.int64).max else object
alpha = field.primitive_element()
Expand Down Expand Up @@ -248,6 +249,19 @@ def make_luts(field, folder, sparse=False):
d = {"X": X, "Y": Y, "Z": Z}
save_pickle(d, folder, "poly_multiply.pkl")

X = [random_coeffs(0, order, MIN_COEFFS, MAX_COEFFS) for i in range(20)]
Y = [random.randint(1, 2*characteristic) for i in range(20)]
Z = []
for i in range(len(X)):
x = ring([F(e) for e in X[i][::-1]])
y = Y[i]
z = x * y
z = np.array([I(e) for e in z.list()[::-1]], dtype=dtype).tolist()
z = z if z != [] else [0]
Z.append(z)
d = {"X": X, "Y": Y, "Z": Z}
save_pickle(d, folder, "poly_scalar_multiply.pkl")

X = [random_coeffs(0, order, MIN_COEFFS, MAX_COEFFS) for i in range(20)]
Y = [random_coeffs(0, order, MIN_COEFFS, MAX_COEFFS) for i in range(20)]
# Add some specific polynomial types
Expand Down
Binary file modified tests/data/fields/GF(109987^4)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(109987^4)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(109987^4)/poly_power.pkl
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/fields/GF(2)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(2)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2147483647)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2147483647)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2147483647)/poly_power.pkl
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/fields/GF(2^100)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^100)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^100)/poly_power.pkl
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/fields/GF(2^2)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^2)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^2)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(2^2)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^3)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^3)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^3)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(2^3)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^32)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^32)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^32)/poly_power.pkl
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/fields/GF(2^8)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^8)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^8)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(2^8)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^8, 283, 19)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^8, 283, 19)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(2^8, 283, 19)/poly_power.pkl
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/fields/GF(31)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(31)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(31)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(31)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(3191)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(3191)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(3191)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(3191)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(36893488147419103183)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(36893488147419103183)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(36893488147419103183)/poly_power.pkl
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/fields/GF(5)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(5)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(5)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(5)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(7)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7^3)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7^3)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7^3)/poly_power.pkl
Binary file not shown.
Binary file added tests/data/fields/GF(7^3)/poly_scalar_multiply.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7^3, 643, 244)/poly_divmod.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7^3, 643, 244)/poly_evaluate.pkl
Binary file not shown.
Binary file modified tests/data/fields/GF(7^3, 643, 244)/poly_power.pkl
Binary file not shown.
Binary file not shown.
40 changes: 37 additions & 3 deletions tests/polys/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,53 @@ def load_poly_luts(name, GF, folder):
@pytest.fixture(scope="session")
def poly_add(field_folder):
GF, folder = field_folder
return load_poly_luts("poly_add.pkl", GF, folder)
with open(os.path.join(folder, "poly_add.pkl"), "rb") as f:
print(f"Loading {f}...")
d = pickle.load(f)
d["GF"] = GF
d["X"] = [galois.Poly(p, field=GF) for p in d["X"]]
d["Y"] = [galois.Poly(p, field=GF) for p in d["Y"]]
d["Z"] = [galois.Poly(p, field=GF) for p in d["Z"]]
return d


@pytest.fixture(scope="session")
def poly_subtract(field_folder):
GF, folder = field_folder
return load_poly_luts("poly_subtract.pkl", GF, folder)
with open(os.path.join(folder, "poly_subtract.pkl"), "rb") as f:
print(f"Loading {f}...")
d = pickle.load(f)
d["GF"] = GF
d["X"] = [galois.Poly(p, field=GF) for p in d["X"]]
d["Y"] = [galois.Poly(p, field=GF) for p in d["Y"]]
d["Z"] = [galois.Poly(p, field=GF) for p in d["Z"]]
return d


@pytest.fixture(scope="session")
def poly_multiply(field_folder):
GF, folder = field_folder
return load_poly_luts("poly_multiply.pkl", GF, folder)
with open(os.path.join(folder, "poly_multiply.pkl"), "rb") as f:
print(f"Loading {f}...")
d = pickle.load(f)
d["GF"] = GF
d["X"] = [galois.Poly(p, field=GF) for p in d["X"]]
d["Y"] = [galois.Poly(p, field=GF) for p in d["Y"]]
d["Z"] = [galois.Poly(p, field=GF) for p in d["Z"]]
return d


@pytest.fixture(scope="session")
def poly_scalar_multiply(field_folder):
GF, folder = field_folder
with open(os.path.join(folder, "poly_scalar_multiply.pkl"), "rb") as f:
print(f"Loading {f}...")
d = pickle.load(f)
d["GF"] = GF
d["X"] = [galois.Poly(p, field=GF) for p in d["X"]]
d["Y"] = d["Y"]
d["Z"] = [galois.Poly(p, field=GF) for p in d["Z"]]
return d


@pytest.fixture(scope="session")
Expand Down
Loading

0 comments on commit 70dc6e2

Please sign in to comment.