# Array Arithmetic¶

After creating a FieldArray subclass and one or two arrays, nearly any arithmetic operation can be performed using normal NumPy ufuncs or Python operators.

In the sections below, the finite field $$\mathrm{GF}(3^5)$$ and arrays $$x$$ and $$y$$ are used.

In [1]: GF = galois.GF(3**5)

In [2]: x = GF([184, 25, 157, 31]); x
Out[2]: GF([184,  25, 157,  31], order=3^5)

In [3]: y = GF([179, 9, 139, 27]); y
Out[3]: GF([179,   9, 139,  27], order=3^5)


## Ufuncs¶

NumPy ufuncs are universal functions that operate on scalars. Unary ufuncs operate on a single scalar and binary ufuncs operate on two scalars. NumPy extends the scalar operation of ufuncs to operate on arrays in various ways. This extensibility enables NumPy broadcasting.

Expand any section for more details.

Addition: x + y == np.add(x, y)

In [4]: x + y
Out[4]: GF([ 81,   7, 215,  58], order=3^5)

Out[5]: GF([ 81,   7, 215,  58], order=3^5)


Additive inverse: -x == np.negative(x)

In [6]: -x
Out[6]: GF([ 98,  14, 206,  62], order=3^5)

In [7]: np.negative(x)
Out[7]: GF([ 98,  14, 206,  62], order=3^5)


In [8]: x + np.negative(x)
Out[8]: GF([0, 0, 0, 0], order=3^5)


Subtraction: x - y == np.subtract(x, y)

In [9]: x - y
Out[9]: GF([17, 16, 18,  4], order=3^5)

In [10]: np.subtract(x, y)
Out[10]: GF([17, 16, 18,  4], order=3^5)


Multiplication: x * y == np.multiply(x, y)

In [11]: x * y
Out[11]: GF([ 41, 225, 106, 123], order=3^5)

In [12]: np.multiply(x, y)
Out[12]: GF([ 41, 225, 106, 123], order=3^5)


Scalar multiplication: x * z == np.multiply(x, z)

Scalar multiplication is essentially repeated addition. It is the “multiplication” of finite field elements and integers. The integer value indicates how many additions of the field element to sum.

In [13]: x * 4
Out[13]: GF([184,  25, 157,  31], order=3^5)

In [14]: np.multiply(x, 4)
Out[14]: GF([184,  25, 157,  31], order=3^5)

In [15]: x + x + x + x
Out[15]: GF([184,  25, 157,  31], order=3^5)


In finite fields $$\mathrm{GF}(p^m)$$, the characteristic $$p$$ is the smallest value when multiplied by any non-zero field element that results in 0.

In [16]: p = GF.characteristic; p
Out[16]: 3

In [17]: x * p
Out[17]: GF([0, 0, 0, 0], order=3^5)


Multiplicative inverse: y ** -1 == np.reciprocal(y)

In [18]: y ** -1
Out[18]: GF([ 71, 217, 213, 235], order=3^5)

In [19]: GF(1) / y
Out[19]: GF([ 71, 217, 213, 235], order=3^5)

In [20]: np.reciprocal(y)
Out[20]: GF([ 71, 217, 213, 235], order=3^5)


Any array multiplied by its multiplicative inverse results in one.

In [21]: y * np.reciprocal(y)
Out[21]: GF([1, 1, 1, 1], order=3^5)


Division: x / y == x // y == np.divide(x, y)

In [22]: x / y
Out[22]: GF([237,  56, 122, 126], order=3^5)

In [23]: x // y
Out[23]: GF([237,  56, 122, 126], order=3^5)

In [24]: np.divide(x, y)
Out[24]: GF([237,  56, 122, 126], order=3^5)


Remainder: x % y == np.remainder(x, y)

In [25]: x % y
Out[25]: GF([0, 0, 0, 0], order=3^5)

In [26]: np.remainder(x, y)
Out[26]: GF([0, 0, 0, 0], order=3^5)


Divmod: divmod(x, y) == np.divmod(x, y)

In [27]: x / y, x % y
Out[27]: (GF([237,  56, 122, 126], order=3^5), GF([0, 0, 0, 0], order=3^5))

In [28]: divmod(x, y)
Out[28]: (GF([237,  56, 122, 126], order=3^5), GF([0, 0, 0, 0], order=3^5))

In [29]: np.divmod(x, y)
Out[29]: (GF([237,  56, 122, 126], order=3^5), GF([0, 0, 0, 0], order=3^5))

In [30]: q, r = divmod(x, y)

In [31]: q*y + r == x
Out[31]: array([ True,  True,  True,  True])


Exponentiation: x ** z == np.power(x, z)

In [32]: x ** 3
Out[32]: GF([175,  76, 218, 192], order=3^5)

In [33]: np.power(x, 3)
Out[33]: GF([175,  76, 218, 192], order=3^5)

In [34]: x * x * x
Out[34]: GF([175,  76, 218, 192], order=3^5)


Square root: np.sqrt(x)

# Ensure the elements of x have square roots
Out[35]: array([ True,  True,  True,  True])

In [36]: z = np.sqrt(x); z
Out[36]: GF([102, 109,  14, 111], order=3^5)

In [37]: z ** 2 == x
Out[37]: array([ True,  True,  True,  True])


Logarithm: np.log(x)

In [38]: z = np.log(y); z
Out[38]: array([60,  2, 59,  3])

In [39]: α = GF.primitive_element; α
Out[39]: GF(3, order=3^5)

In [40]: α ** z == y
Out[40]: array([ True,  True,  True,  True])


## Ufunc methods¶

FieldArray instances support NumPy ufunc methods. Ufunc methods allow a user to apply a NumPy ufunc in a unique way across the target array. All arithmetic ufuncs are supported.

Expand any section for more details.

reduce()

The reduce methods reduce the input array’s dimension by one, applying the ufunc across one axis.

In [41]: np.add.reduce(x)
Out[41]: GF(7, order=3^5)

In [42]: x[0] + x[1] + x[2] + x[3]
Out[42]: GF(7, order=3^5)

In [43]: np.multiply.reduce(x)
Out[43]: GF(105, order=3^5)

In [44]: x[0] * x[1] * x[2] * x[3]
Out[44]: GF(105, order=3^5)


accumulate()

The accumulate methods accumulate the result of the ufunc across a specified axis.

In [45]: np.add.accumulate(x)
Out[45]: GF([184, 173,  57,   7], order=3^5)

In [46]: GF([x[0], x[0] + x[1], x[0] + x[1] + x[2], x[0] + x[1] + x[2] + x[3]])
Out[46]: GF([184, 173,  57,   7], order=3^5)

In [47]: np.multiply.accumulate(x)
Out[47]: GF([184,   9, 211, 105], order=3^5)

In [48]: GF([x[0], x[0] * x[1], x[0] * x[1] * x[2], x[0] * x[1] * x[2] * x[3]])
Out[48]: GF([184,   9, 211, 105], order=3^5)


reduceat()

The reduceat methods reduces the input array’s dimension by one, applying the ufunc across one axis in-between certain indices.

In [49]: np.add.reduceat(x, [0, 3])
Out[49]: GF([57, 31], order=3^5)

In [50]: GF([x[0] + x[1] + x[2], x[3]])
Out[50]: GF([57, 31], order=3^5)

In [51]: np.multiply.reduceat(x, [0, 3])
Out[51]: GF([211,  31], order=3^5)

In [52]: GF([x[0] * x[1] * x[2], x[3]])
Out[52]: GF([211,  31], order=3^5)


outer()

The outer methods applies the ufunc to each pair of inputs.

In [53]: np.add.outer(x, y)
Out[53]:
GF([[ 81, 166,  80, 211],
[165,   7, 155,  52],
[ 54, 139, 215, 103],
[198,  40,  89,  58]], order=3^5)

In [54]: np.multiply.outer(x, y)
Out[54]:
GF([[ 41, 192,  93,  97],
[ 91, 225, 193, 196],
[ 80, 211, 106, 145],
[149,  41, 129, 123]], order=3^5)


at()

The at methods performs the ufunc in-place at the specified indices.

In [55]: z = x.copy()

# Negate indices 0 and 1 in-place
In [56]: np.negative.at(x, [0, 1]); x
Out[56]: GF([ 98,  14, 157,  31], order=3^5)

In [57]: z[0:1] *= -1; z
Out[57]: GF([ 98,  25, 157,  31], order=3^5)


Convolution: np.convolve(x, y)

In [58]: np.convolve(x, y)
Out[58]: GF([ 79,  80,   5,  79, 167, 166, 123], order=3^5)


FFT: np.fft.fft(x)

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$$.

In [59]: GF = galois.GF(7**5)

In [60]: n = 6

# n divides p^m - 1
In [61]: (GF.order - 1) % n
Out[61]: 0

In [62]: x = GF.Random(n); x
Out[62]: GF([ 6575, 11705, 15797,  5712,  1941, 15867], order=7^5)

In [63]: X = np.fft.fft(x); X
Out[63]: GF([16276, 14293, 11500,  7934,  6299, 14816], order=7^5)

In [64]: np.fft.ifft(X)
Out[64]: GF([ 6575, 11705, 15797,  5712,  1941, 15867], order=7^5)


See also ntt() and primitive_root_of_unity.

Inverse FFT: np.fft.ifft(X)

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$$.

In [65]: GF = galois.GF(7**5)

In [66]: n = 6

# n divides p^m - 1
In [67]: (GF.order - 1) % n
Out[67]: 0

In [68]: x = GF.Random(n); x
Out[68]: GF([ 9332,  4902, 10078,  1003, 15219,  1031], order=7^5)

In [69]: X = np.fft.fft(x); X
Out[69]: GF([ 2701, 15065,   114, 11180,  3739, 14576], order=7^5)

In [70]: np.fft.ifft(X)
Out[70]: GF([ 9332,  4902, 10078,  1003, 15219,  1031], order=7^5)


See also ntt() and primitive_root_of_unity.

## Linear algebra¶

Linear algebra on FieldArray arrays/matrices is supported through both native NumPy linear algebra functions in numpy.linalg and additional linear algebra routines not included in NumPy.

Expand any section for more details.

Dot product: np.dot(a, b)

In [71]: GF = galois.GF(31)

In [72]: a = GF([29, 0, 2, 21]); a
Out[72]: GF([29,  0,  2, 21], order=31)

In [73]: b = GF([23, 5, 15, 12]); b
Out[73]: GF([23,  5, 15, 12], order=31)

In [74]: np.dot(a, b)
Out[74]: GF(19, order=31)


Vector dot product: np.vdot(a, b)

In [75]: GF = galois.GF(31)

In [76]: a = GF([29, 0, 2, 21]); a
Out[76]: GF([29,  0,  2, 21], order=31)

In [77]: b = GF([23, 5, 15, 12]); b
Out[77]: GF([23,  5, 15, 12], order=31)

In [78]: np.vdot(a, b)
Out[78]: GF(19, order=31)


Inner product: np.inner(a, b)

In [79]: GF = galois.GF(31)

In [80]: a = GF([29, 0, 2, 21]); a
Out[80]: GF([29,  0,  2, 21], order=31)

In [81]: b = GF([23, 5, 15, 12]); b
Out[81]: GF([23,  5, 15, 12], order=31)

In [82]: np.inner(a, b)
Out[82]: GF(19, order=31)


Outer product: np.outer(a, b)

In [83]: GF = galois.GF(31)

In [84]: a = GF([29, 0, 2, 21]); a
Out[84]: GF([29,  0,  2, 21], order=31)

In [85]: b = GF([23, 5, 15, 12]); b
Out[85]: GF([23,  5, 15, 12], order=31)

In [86]: np.outer(a, b)
Out[86]:
GF([[16, 21,  1,  7],
[ 0,  0,  0,  0],
[15, 10, 30, 24],
[18, 12,  5,  4]], order=31)


Matrix multiplication: A @ B == np.matmul(A, B)

In [87]: GF = galois.GF(31)

In [88]: A = GF([[17, 25, 18, 8], [7, 9, 21, 15], [6, 16, 6, 30]]); A
Out[88]:
GF([[17, 25, 18,  8],
[ 7,  9, 21, 15],
[ 6, 16,  6, 30]], order=31)

In [89]: B = GF([[8, 18], [22, 0], [7, 8], [20, 24]]); B
Out[89]:
GF([[ 8, 18],
[22,  0],
[ 7,  8],
[20, 24]], order=31)

In [90]: A @ B
Out[90]:
GF([[11, 22],
[19,  3],
[19,  8]], order=31)

In [91]: np.matmul(A, B)
Out[91]:
GF([[11, 22],
[19,  3],
[19,  8]], order=31)


Matrix exponentiation: np.linalg.matrix_power(A, z)

In [92]: GF = galois.GF(31)

In [93]: A = GF([[14, 1, 5], [3, 23, 6], [24, 27, 4]]); A
Out[93]:
GF([[14,  1,  5],
[ 3, 23,  6],
[24, 27,  4]], order=31)

In [94]: np.linalg.matrix_power(A, 3)
Out[94]:
GF([[ 1, 16,  4],
[11,  9,  9],
[ 8, 24, 29]], order=31)

In [95]: A @ A @ A
Out[95]:
GF([[ 1, 16,  4],
[11,  9,  9],
[ 8, 24, 29]], order=31)


Matrix determinant: np.linalg.det(A)

In [96]: GF = galois.GF(31)

In [97]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[97]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [98]: np.linalg.det(A)
Out[98]: GF(0, order=31)


Matrix rank: np.linalg.matrix_rank(A, z)

In [99]: GF = galois.GF(31)

In [100]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[100]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [101]: np.linalg.matrix_rank(A)
Out[101]: 3

In [102]: A.row_reduce()
Out[102]:
GF([[ 1,  0,  0, 11],
[ 0,  1,  0, 25],
[ 0,  0,  1, 11],
[ 0,  0,  0,  0]], order=31)


Matrix trace: np.trace(A)

In [103]: GF = galois.GF(31)

In [104]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[104]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [105]: np.trace(A)
Out[105]: GF(0, order=31)

In [106]: A[0,0] + A[1,1] + A[2,2] + A[3,3]
Out[106]: GF(0, order=31)


Solve a system of equations: np.linalg.solve(A, b)

In [107]: GF = galois.GF(31)

In [108]: A = GF([[14, 21, 14, 28], [24, 22, 23, 23], [16, 30, 26, 18], [4, 23, 18, 3]]); A
Out[108]:
GF([[14, 21, 14, 28],
[24, 22, 23, 23],
[16, 30, 26, 18],
[ 4, 23, 18,  3]], order=31)

In [109]: b = GF([15, 11, 6, 29]); b
Out[109]: GF([15, 11,  6, 29], order=31)

In [110]: x = np.linalg.solve(A, b)

In [111]: np.array_equal(A @ x, b)
Out[111]: True


Matrix inverse: np.linalg.inv(A)

In [112]: GF = galois.GF(31)

In [113]: A = GF([[14, 21, 14, 28], [24, 22, 23, 23], [16, 30, 26, 18], [4, 23, 18, 3]]); A
Out[113]:
GF([[14, 21, 14, 28],
[24, 22, 23, 23],
[16, 30, 26, 18],
[ 4, 23, 18,  3]], order=31)

In [114]: A_inv = np.linalg.inv(A); A_inv
Out[114]:
GF([[27, 17,  9,  8],
[20, 21, 12,  4],
[30, 10, 23, 22],
[13, 25,  6, 13]], order=31)

In [115]: A @ A_inv
Out[115]:
GF([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]], order=31)


Below are additional linear algebra routines provided for FieldArray arrays/matrices that are not included in NumPy.

Row space: A.row_space()

In [116]: GF = galois.GF(31)

In [117]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[117]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [118]: A.row_space()
Out[118]:
GF([[ 1,  0,  0, 11],
[ 0,  1,  0, 25],
[ 0,  0,  1, 11]], order=31)


See row_space() for more details.

Column space: A.column_space()

In [119]: GF = galois.GF(31)

In [120]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[120]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [121]: A.column_space()
Out[121]:
GF([[ 1,  0,  0,  0],
[ 0,  1,  0, 11],
[ 0,  0,  1,  5]], order=31)


See column_space() for more details.

Left null space: A.left_null_space()

In [122]: GF = galois.GF(31)

In [123]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[123]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [124]: A.left_null_space()
Out[124]: GF([[ 0,  1, 23, 14]], order=31)


See left_null_space() for more details.

Null space: A.null_space()

In [125]: GF = galois.GF(31)

In [126]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[126]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [127]: A.null_space()
Out[127]: GF([[ 1, 22,  1, 14]], order=31)


See null_space() for more details.

Gaussian elimination: A.row_reduce()

In [128]: GF = galois.GF(31)

In [129]: A = GF([[23, 11, 3, 3], [13, 6, 16, 4], [12, 10, 5, 3], [17, 23, 15, 28]]); A
Out[129]:
GF([[23, 11,  3,  3],
[13,  6, 16,  4],
[12, 10,  5,  3],
[17, 23, 15, 28]], order=31)

In [130]: A.row_reduce()
Out[130]:
GF([[ 1,  0,  0, 11],
[ 0,  1,  0, 25],
[ 0,  0,  1, 11],
[ 0,  0,  0,  0]], order=31)


See row_reduce() for more details.

LU decomposition: A.lu_decompose()

In [131]: GF = galois.GF(31)

In [132]: A = GF([[4, 1, 24], [7, 6, 1], [11, 20, 2]]); A
Out[132]:
GF([[ 4,  1, 24],
[ 7,  6,  1],
[11, 20,  2]], order=31)

In [133]: L, U = A.lu_decompose()

In [134]: L
Out[134]:
GF([[ 1,  0,  0],
[25,  1,  0],
[26, 15,  1]], order=31)

In [135]: U
Out[135]:
GF([[ 4,  1, 24],
[ 0, 12, 21],
[ 0,  0, 24]], order=31)

In [136]: np.array_equal(L @ U, A)
Out[136]: True


See lu_decompose() for more details.

PLU decomposition: A.plu_decompose()

In [137]: GF = galois.GF(31)

In [138]: A = GF([[15, 4, 11], [7, 6, 1], [11, 20, 2]]); A
Out[138]:
GF([[15,  4, 11],
[ 7,  6,  1],
[11, 20,  2]], order=31)

In [139]: P, L, U = A.plu_decompose()

In [140]: P
Out[140]:
GF([[1, 0, 0],
[0, 0, 1],
[0, 1, 0]], order=31)

In [141]: L
Out[141]:
GF([[ 1,  0,  0],
[ 9,  1,  0],
[17,  0,  1]], order=31)

In [142]: U
Out[142]:
GF([[15,  4, 11],
[ 0, 15, 27],
[ 0,  0,  0]], order=31)

In [143]: np.array_equal(P @ L @ U, A)
Out[143]: True


See plu_decompose() for more details.

Last update: Jul 24, 2022