Skip to content

Commit

Permalink
Greatly clean-up Galois field arithmetic functions/classes
Browse files Browse the repository at this point in the history
  • Loading branch information
mhostetter committed Aug 21, 2021
1 parent a787439 commit fa00388
Show file tree
Hide file tree
Showing 14 changed files with 871 additions and 1,638 deletions.
10 changes: 5 additions & 5 deletions galois/_codes/_bch.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,11 @@ def __new__(cls, n, k, primitive_poly=None, primitive_element=None, systematic=T

def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
# Pre-compile the arithmetic methods
self._add_jit = self.field._calculate_jit("add")
self._subtract_jit = self.field._calculate_jit("subtract")
self._multiply_jit = self.field._calculate_jit("multiply")
self._reciprocal_jit = self.field._calculate_jit("reciprocal")
self._power_jit = self.field._calculate_jit("power")
self._add_jit = self.field._func_calculate("add")
self._subtract_jit = self.field._func_calculate("subtract")
self._multiply_jit = self.field._func_calculate("multiply")
self._reciprocal_jit = self.field._func_calculate("reciprocal")
self._power_jit = self.field._func_calculate("power")

# Pre-compile the JIT functions
self._berlekamp_massey_jit = _lfsr.jit_calculate("berlekamp_massey")
Expand Down
10 changes: 5 additions & 5 deletions galois/_codes/_reed_solomon.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ def __new__(cls, n, k, c=1, primitive_poly=None, primitive_element=None, systema

def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
# Pre-compile the arithmetic methods
self._add_jit = self.field._calculate_jit("add")
self._subtract_jit = self.field._calculate_jit("subtract")
self._multiply_jit = self.field._calculate_jit("multiply")
self._reciprocal_jit = self.field._calculate_jit("reciprocal")
self._power_jit = self.field._calculate_jit("power")
self._add_jit = self.field._func_calculate("add")
self._subtract_jit = self.field._func_calculate("subtract")
self._multiply_jit = self.field._func_calculate("multiply")
self._reciprocal_jit = self.field._func_calculate("reciprocal")
self._power_jit = self.field._func_calculate("power")

# Pre-compile the JIT functions
self._berlekamp_massey_jit = _lfsr.jit_calculate("berlekamp_massey")
Expand Down
6 changes: 6 additions & 0 deletions galois/_fields/_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,12 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):

def __str__(self):
return self.__repr__()
# formatter = type(self)._formatter(self)

# with np.printoptions(formatter=formatter):
# string = super().__str__()

# return string

def __repr__(self):
formatter = type(self)._formatter(self)
Expand Down
171 changes: 97 additions & 74 deletions galois/_fields/_calculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ class CalculateMeta(PropertiesMeta):
"""
# pylint: disable=no-value-for-parameter

# Function signatures for JIT-compiled arithmetic functions
# Function signatures for JIT-compiled "calculate" arithmetic functions
_UNARY_CALCULATE_SIG = numba.types.FunctionType(int64(int64, int64, int64, int64))
_BINARY_CALCULATE_SIG = numba.types.FunctionType(int64(int64, int64, int64, int64, int64))

# Lookup table of ufuncs to unary/binary type needed for LookupMeta, CalculateMeta, etc
_FUNC_CACHE_CALCULATE = {}
_UFUNC_CACHE_CALCULATE = {}

_UFUNC_TYPE = {
"add": "binary",
"negative": "unary",
Expand All @@ -30,102 +32,123 @@ class CalculateMeta(PropertiesMeta):
"log": "binary",
}

###############################################################################
# Individual JIT arithmetic functions, pre-compiled (cached)
###############################################################################
def __init__(cls, name, bases, namespace, **kwargs):
super().__init__(name, bases, namespace, **kwargs)
cls._reset_globals()

def _calculate_jit(cls, name):
def _set_globals(cls, name): # pylint: disable=unused-argument,no-self-use
"""
To be implemented in GF2Meta, GF2mMeta, GFpMeta, and GFpmMeta.
Sets the global variables in the GF*Meta metaclass mixins that are needed for linking during JIT compilation.
"""
raise NotImplementedError
return

def _python_func(cls, name):
def _reset_globals(cls): # pylint: disable=no-self-use
"""
To be implemented in GF2Meta, GF2mMeta, GFpMeta, and GFpmMeta.
Resets the global variables in the GF*Meta metaclass mixins so when the pure-python functions/ufuncs invoke these
globals, they reference the correct pure-python functions and not the JIT-compiled functions/ufuncs.
"""
raise NotImplementedError
return

###############################################################################
# Individual ufuncs, compiled on-demand
###############################################################################
def _func_calculate(cls, name, reset=True):
"""
Returns a JIT-compiled arithmetic function using explicit calculation. These functions are once-compiled and shared for all
Galois fields. The only difference between Galois fields are the characteristic, degree, and irreducible polynomial that are
passed in as inputs.
"""
key = (name,)

def _calculate_ufunc(cls, name):
raise NotImplementedError
if key not in cls._FUNC_CACHE_CALCULATE:
cls._set_globals(name)
function = getattr(cls, f"_{name}_calculate")

def _python_ufunc(cls, name): # pylint: disable=no-self-use
function = eval(f"cls._{name}_python")
if cls._UFUNC_TYPE[name] == "unary":
cls._FUNC_CACHE_CALCULATE[key] = numba.jit(["int64(int64, int64, int64, int64)"], nopython=True, cache=True)(function)
else:
cls._FUNC_CACHE_CALCULATE[key] = numba.jit(["int64(int64, int64, int64, int64, int64)"], nopython=True, cache=True)(function)

if reset:
cls._reset_globals()

return cls._FUNC_CACHE_CALCULATE[key]

def _ufunc_calculate(cls, name):
"""
Returns a JIT-compiled arithmetic ufunc using explicit calculation. These ufuncs are compiled for each Galois field since
the characteristic, degree, and irreducible polynomial are compiled into the ufuncs as constants.
"""
key = (name, cls.characteristic, cls.degree, cls._irreducible_poly_int)

if key not in cls._UFUNC_CACHE_CALCULATE:
cls._set_globals(name)
function = getattr(cls, f"_{name}_calculate")

# These variables must be locals and not class properties for Numba to compile them as literals
CHARACTERISTIC = cls.characteristic
DEGREE = cls.degree
IRREDUCIBLE_POLY = cls._irreducible_poly_int

if cls._UFUNC_TYPE[name] == "unary":
cls._UFUNC_CACHE_CALCULATE[key] = numba.vectorize(["int64(int64)"], nopython=True)(lambda a: function(a, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY))
else:
cls._UFUNC_CACHE_CALCULATE[key] = numba.vectorize(["int64(int64, int64)"], nopython=True)(lambda a, b: function(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY))

cls._reset_globals()

return cls._UFUNC_CACHE_CALCULATE[key]

def _func_python(cls, name):
"""
Returns a pure-python arithmetic function using explicit calculation. This lambda function wraps the arithmetic functions in
GF2Meta, GF2mMeta, GFpMeta, and GFpmMeta by passing in the field's characteristic, degree, and irreducible polynomial.
"""
return getattr(cls, f"_{name}_calculate")
# if cls._UFUNC_TYPE[name] == "unary":
# return lambda a: function(a, cls.characteristic, cls.degree, cls._irreducible_poly_int)
# else:
# return lambda a, b: function(a, b, cls.characteristic, cls.degree, cls._irreducible_poly_int)

def _ufunc_python(cls, name):
"""
Returns a pure-python arithmetic ufunc using explicit calculation.
"""
function = getattr(cls, f"_{name}_calculate")
if cls._UFUNC_TYPE[name] == "unary":
return np.frompyfunc(function, 1, 1)
return np.frompyfunc(lambda a: function(a, cls.characteristic, cls.degree, cls._irreducible_poly_int), 1, 1)
else:
return np.frompyfunc(function, 2, 1)
return np.frompyfunc(lambda a, b: function(a, b, cls.characteristic, cls.degree, cls._irreducible_poly_int), 2, 1)

###############################################################################
# Pure-python arithmetic methods using explicit calculation
# Arithmetic functions using explicit calculation
###############################################################################

def _add_python(cls, a, b):
@staticmethod
def _add_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

def _negative_python(cls, a):
@staticmethod
def _negative_calculate(a, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

def _subtract_python(cls, a, b):
@staticmethod
def _subtract_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

def _multiply_python(cls, a, b):
@staticmethod
def _multiply_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

def _reciprocal_python(cls, a):
@staticmethod
def _reciprocal_calculate(a, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

def _divide_python(cls, a, b):
if b == 0:
raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")

if a == 0:
return 0
else:
b_inv = cls._reciprocal_python(b)
return cls._multiply_python(a, b_inv)

def _power_python(cls, a, b):
if a == 0 and b < 0:
raise ZeroDivisionError("Cannot compute the multiplicative inverse of 0 in a Galois field.")

if b == 0:
return 1
elif b < 0:
a = cls._reciprocal_python(a)
b = abs(b)

result_s = a # The "squaring" part
result_m = 1 # The "multiplicative" part

while b > 1:
if b % 2 == 0:
result_s = cls._multiply_python(result_s, result_s)
b //= 2
else:
result_m = cls._multiply_python(result_m, result_s)
b -= 1

result = cls._multiply_python(result_m, result_s)

return result

def _log_python(cls, a, b):
"""
TODO: Replace this with a more efficient algorithm
"""
if a == 0:
raise ArithmeticError("Cannot compute the discrete logarithm of 0 in a Galois field.")
@staticmethod
def _divide_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

# Naive algorithm
result = 1
for i in range(0, cls.order - 1):
if result == a:
break
result = cls._multiply_python(result, b)
@staticmethod
def _power_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError

return i
@staticmethod
def _log_calculate(a, b, CHARACTERISTIC, DEGREE, IRREDUCIBLE_POLY):
raise NotImplementedError
28 changes: 1 addition & 27 deletions galois/_fields/_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class FieldClass(FunctionMeta, UfuncMeta, PropertiesMeta):
This metaclass gives :obj:`galois.FieldArray` subclasses returned from the class factory :func:`galois.GF` methods and attributes
related to its Galois field.
"""
# pylint: disable=no-value-for-parameter
# pylint: disable=no-value-for-parameter,unsupported-membership-test,abstract-method

def __new__(cls, name, bases, namespace, **kwargs): # pylint: disable=unused-argument
return super().__new__(cls, name, bases, namespace)
Expand All @@ -34,32 +34,6 @@ def __str__(cls):
def __repr__(cls):
return str(cls)

###############################################################################
# Individual JIT arithmetic functions, pre-compiled (cached)
###############################################################################

def _calculate_jit(cls, name):
"""
To be implemented in GF2Meta, GF2mMeta, GFpMeta, and GFpmMeta.
"""
raise NotImplementedError

def _python_func(cls, name):
"""
To be implemented in GF2Meta, GF2mMeta, GFpMeta, and GFpmMeta.
"""
raise NotImplementedError

###############################################################################
# Individual ufuncs, compiled on-demand
###############################################################################

def _calculate_ufunc(cls, name):
"""
To be implemented in GF2Meta, GF2mMeta, GFpMeta, and GFpmMeta.
"""
raise NotImplementedError

###############################################################################
# Class methods
###############################################################################
Expand Down
Loading

0 comments on commit fa00388

Please sign in to comment.