From 91f4191a65102ec77792963f9bde18904de3ad8a Mon Sep 17 00:00:00 2001 From: Niklas Birk Date: Wed, 14 Feb 2024 23:51:35 +0100 Subject: [PATCH] Rewrite matrix.py to match test_serial.py and to be less dependent on numpy --- src/matrix.py | 170 ++++++++++++++++++++++++++++++++------------ test/test_matrix.py | 81 +++++++++++---------- test/test_serial.py | 26 +++---- 3 files changed, 184 insertions(+), 93 deletions(-) diff --git a/src/matrix.py b/src/matrix.py index 611921e..5397452 100644 --- a/src/matrix.py +++ b/src/matrix.py @@ -1,3 +1,5 @@ +import math + import numpy from numpy import linalg @@ -6,16 +8,18 @@ class Matrix: """ This Matrix class represents a real 2D-matrix. """ - __data__: numpy.ndarray + __data__: list __shape__: (int, int) - def __init__(self, data=None, shape=None, structure=None, model=None, n=None): + def __init__(self, data=None, shape=None, structure=None, model=None, offset=None, n=None): """ Creates a new matrix. - Thy type of the matrix depends on the signature and arguments. + The type of the matrix depends on the signature and arguments. + - ``Matrix(list)``: will create a new matrix with the given data in the list and its shape. - ``Matrix(numpy.ndarray)``: will create a new matrix with the given data in ndarray and its shape. - ``Matrix(list, (int,int))``: will create a new nxm matrix with the given rows and columns and data in list. + - ``Matrix(list, str, int, int)``: will create a new square matrix of given size and structure of \"diagonal\" - ``Matrix(list, str, int)``: will create a new square matrix of given size and structure of either \"unity\", \"diagonal\" or \"tridiagonal\" - ``Matrix(str, int)``: will create a new square matrix of given size and TODO @@ -24,12 +28,14 @@ class Matrix: :param shape: A tuple containing the amount of rows and columns :param structure: Either \"unity\", \"diagonal\" or \"tridiagonal\" :param model: TODO + :param offset: Offset to diagonal axis :param n: Amount of rows of a square matrix or offset in case of diagonal structure :type data: list | numpy.ndarray :type shape: (int, int) :type structure: str :type model: str + :type offset: int :type n: int :rtype: Matrix @@ -37,41 +43,46 @@ class Matrix: # Case Matrix(str, int) if n is not None and model is not None: ... # TODO: what shall one do here? + # Case: Matrix(list, str, int, int) + elif n is not None and offset is not None and structure == "diagonal" and data is not None: + diag = numpy.diag(data * (n - abs(offset)), offset) + self.__data__ = diag.tolist() + self.__shape__ = diag.shape # Case: Matrix(list, str, int) elif n is not None and structure is not None and data is not None: if structure == "unity": ... # TODO: what does it mean? - elif structure == "diagonal": - diag = numpy.diag(data, n) - self.__data__ = diag - self.__shape__ = diag.shape elif structure == "tridiagonal": if len(data) != 3: raise ValueError("If structure is tridiagonal, then the given data must be of length 3") tridiag = numpy.diag([data[0]] * (n - 1), -1) + numpy.diag([data[1]] * n, 0) + numpy.diag( [data[2]] * (n - 1), 1) - self.__data__ = tridiag + self.__data__ = tridiag.tolist() self.__shape__ = tridiag.shape - # Case: Matrix(list, str, int) + # Case: Matrix(list, (int,int)) elif shape is not None and data is not None: self.__shape__ = shape - self.__data__ = numpy.array(data).reshape(shape) - # Case: Matrix(numpy.ndarray) + self.__data__ = numpy.array(data).reshape(shape).tolist() + # Case: Matrix(numpy.ndarray) or Matrix(list) elif data is not None: - try: - data.shape[1] - except IndexError: - self.__shape__ = (data.shape[0], 1) - else: - self.__shape__ = data.shape + if isinstance(data, numpy.ndarray): + try: + data.shape[1] + except IndexError: + self.__shape__ = (data.shape[0], 1) + else: + self.__shape__ = (data.shape[0], data.shape[1]) + elif isinstance(data, list): + self.__shape__ = (len(data), len(data[0])) self.__data__ = data else: raise ValueError( - "Only following signatures are allowed: (numpy.ndarray), (list, tuple), (list, str, int), (str, int)") + "Only following signatures are allowed: " + "(list), (numpy.ndarray), (list, tuple), (list, str, int), (list, str, int, int), (str, int)") def get_data(self): """ - :return: the data of the matrix as a ``numpy.ndarray`` + :return: the data of the matrix as a ``list`` """ return self.__data__ @@ -85,7 +96,16 @@ class Matrix: """ :return: the transpose of the matrix """ - return Matrix(self.__data__.transpose()) + rows = self.__shape__[0] + cols = self.__shape__[1] + + transposed_data = [[0 for _ in range(rows)] for _ in range(cols)] + + for i in range(rows): + for j in range(cols): + transposed_data[j][i] = self.__data__[i][j] + + return Matrix(transposed_data, (cols, rows)) def T(self): """ @@ -103,28 +123,49 @@ class Matrix: :return: True if data in the matrix are equal to the given data in other for each component, otherwise False """ if isinstance(other, Matrix): - to_compare = other.__data__ + data_to_compare = other.__data__ + if self.__shape__ != other.__shape__: + return False elif isinstance(other, list): - to_compare = numpy.array(other) + data_to_compare = other + if self.__shape__[0] != len(other) or self.__shape__[1] != len(other[0]): + return False elif isinstance(other, numpy.ndarray): - to_compare = other + data_to_compare = other.tolist() else: raise ValueError("Matrix type is not comparable to type of given ``other``") - return (self.__data__ == to_compare).all() + + for i in range(len(self.__data__)): + for j in range(len(self.__data__[i])): + if self.__data__[i][j] != data_to_compare[i][j]: + return False + return True def __str__(self): - return str(self.__data__) + return str(numpy.array(self.__data__)) def __neg__(self): - return Matrix(-self.__data__) + rows = range(self.__shape__[0]) + cols = range(self.__shape__[1]) + return Matrix([[-(self.__data__[i][j]) for j in cols] for i in rows], self.__shape__) + + def __add_matrix_internal__(self, other): + rows = self.__shape__[0] + cols = self.__shape__[1] + return [[(self.__data__[i][j] + other.__data__[i][j]) for j in range(cols)] for i in range(rows)] + + def __add_scalar_internal__(self, other): + rows = self.__shape__[0] + cols = self.__shape__[1] + return [[(self.__data__[i][j] + other) for j in range(cols)] for i in range(rows)] def __add__(self, other): if isinstance(other, Matrix): if self.__shape__ != other.__shape__: raise ValueError("The shape of the operands must be the same") - return Matrix(self.__data__ + other.__data__) + return Matrix(self.__add_matrix_internal__(other), self.__shape__) elif isinstance(other, int) or isinstance(other, float): - return Matrix(self.__data__ + other) + return Matrix(self.__add_scalar_internal__(other), self.__shape__) else: raise ValueError("Only a number or another ``Matrix`` can be added to a ``Matrix``") @@ -137,26 +178,47 @@ class Matrix: def __rsub__(self, other): return -self + other + def __truediv_scalar_internal__(self, other): + rows = self.__shape__[0] + cols = self.__shape__[1] + return [[(self.__data__[i][j] / other) for j in range(cols)] for i in range(rows)] + + def __truediv__(self, other): + if isinstance(other, int) or isinstance(other, float): + return Matrix(self.__truediv_scalar_internal__(other), self.__shape__) + else: + raise ValueError("A ``Matrix`` can only be divided ba a number") + + def __mul_matrix_internal__(self, other): + rows = self.__shape__[0] + cols = other.__shape__[1] + + new_data = [[0 for _ in range(rows)] for _ in range(cols)] + + for i in range(rows): + for k in range(cols): + new_data[i][k] = sum([self.__data__[i][j] * other.__data__[j][k] for j in range(self.__shape__[1])]) + return new_data + + def __mul_scalar_internal__(self, other): + cols = range(self.__shape__[1]) + rows = range(self.__shape__[0]) + return [[(self.__data__[i][j] * other) for j in cols] for i in rows] + def __mul__(self, other): if isinstance(other, Matrix): if self.__shape__[1] != other.__shape__[0]: raise ValueError( "The amount of columns of the first operand must match the amount of rows of the second operand") - return Matrix(self.__data__ @ other.__data__) + return Matrix(self.__mul_matrix_internal__(other), (self.__shape__[0], other.__shape__[1])) elif isinstance(other, int) or isinstance(other, float): - return Matrix(other * self.__data__) + return Matrix(self.__mul_scalar_internal__(other), self.__shape__) else: raise ValueError("Only a number or another ``Matrix`` can be multiplied to a ``Matrix``") def __rmul__(self, other): return self * other - def __truediv__(self, other): - if isinstance(other, int) or isinstance(other, float): - return Matrix(self.__data__ / other) - else: - raise ValueError("A ``Matrix`` can only be divided ba a number") - def norm(self, f: str = "frobenius"): """ Calculates the norm of the matrix. @@ -168,15 +230,35 @@ class Matrix: :return: the norm as a number """ - t = "fro" - if f == "colsum": - t = 1 - elif f == "rowsum": - t = numpy.inf - return linalg.norm(self.__data__, t) + norm = 0 + + rows = self.__shape__[0] + cols = self.__shape__[1] + + if f == "frobenius": + abs_sum = 0 + for i in range(rows): + for j in range(cols): + abs_sum += abs(self.__data__[i][j])**2 + norm = math.sqrt(abs_sum) + elif f == "col sum": + row_sum = [0 for _ in range(cols)] + for j in range(cols): + for i in range(rows): + row_sum[j] += abs(self.__data__[i][j]) + norm = max(row_sum) + elif f == "row sum": + col_sum = [0 for _ in range(rows)] + for i in range(rows): + for j in range(cols): + col_sum[i] += abs(self.__data__[i][j]) + norm = max(col_sum) + return norm def __getitem__(self, key): - return self.__data__[key] + return numpy.array(self.__data__)[key].tolist() def __setitem__(self, key, value): - self.__data__[key] = value + manipulated_data = numpy.array(self.__data__) + manipulated_data[key] = value + self.__data__ = manipulated_data.tolist() diff --git a/test/test_matrix.py b/test/test_matrix.py index 4d7c565..071e5d7 100644 --- a/test/test_matrix.py +++ b/test/test_matrix.py @@ -28,17 +28,6 @@ class TestMatrix(TestCase): expected = [[0, 1, 2]] self.assertEqual(expected, actual) - def test_should_create_vectorlike_matrix_from_numpy_array_with_shape_3_1(self): - data = numpy.array([0, 1, 2]) - actual = Matrix(data) - - actual_shape = actual.shape() - expected_shape = (3, 1) - self.assertEqual(expected_shape, actual_shape) - - expected = [0, 1, 2] - self.assertEqual(expected, actual) - def test_should_create_matrix_from_list_with_shape_2_2(self): data = [0, 1, 2, 3] actual = Matrix(data, shape=(2, 2)) @@ -50,9 +39,20 @@ class TestMatrix(TestCase): expected = [[0, 1], [2, 3]] self.assertEqual(expected, actual) + def test_should_create_matrix_from_matrixlike_list_with_shape_2_3(self): + data = [[0, 1, 2], [3, 4, 5]] + actual = Matrix(data, shape=(2, 3)) + + actual_shape = actual.shape() + expected_shape = (2, 3) + self.assertEqual(expected_shape, actual_shape) + + expected = [[0, 1, 2], [3, 4, 5]] + self.assertEqual(expected, actual) + def test_should_create_diagonal_matrix_from_list(self): - data = [1, 1, 1] - actual = Matrix(data, structure="diagonal", n=0) + data = [1] + actual = Matrix(data, structure="diagonal", offset=0, n=3) actual_shape = actual.shape() expected_shape = (3, 3) @@ -62,8 +62,8 @@ class TestMatrix(TestCase): self.assertEqual(expected, actual) def test_should_create_diagonal_matrix_from_list_with_offset_1(self): - data = [1, 1, 1] - actual = Matrix(data, structure="diagonal", n=1) + data = [1] + actual = Matrix(data, structure="diagonal", offset=1, n=4) actual_shape = actual.shape() expected_shape = (4, 4) @@ -188,6 +188,15 @@ class TestMatrix(TestCase): self.assertEqual(expected, actual) + def test_should_div_matrix_by_scalar(self): + m = Matrix([5, 10, 15, 20], (2, 2)) + s = 5 + + actual = m / s + expected = Matrix([1, 2, 3, 4], (2, 2)) + + self.assertEqual(expected, actual) + def test_should_raise_value_missmatch_error_while_dividing_with_other_than_scalar(self): m = Matrix([1, 2, 3, 4], (2, 2)) o = "" @@ -195,20 +204,29 @@ class TestMatrix(TestCase): self.assertRaises(ValueError, lambda: m / o) def test_should_mul_matrices_1(self): - m1 = Matrix([1, 2], (2, 1)) - m2 = Matrix([3, 4], (1, 2)) + m1 = Matrix([1, 2, 3, 4], (2, 2)) + m2 = Matrix([4, 3, 2, 1], (2, 2)) actual = m1 * m2 - expected = Matrix([3, 4, 6, 8], (2, 2)) + expected = Matrix([8, 5, 20, 13], (2, 2)) self.assertEqual(expected, actual) def test_should_mul_matrices_2(self): - m1 = Matrix([1, 2], (1, 2)) - m2 = Matrix([3, 4], (2, 1)) + m1 = Matrix([1, 2, 3, 4, 5, 6], (2, 3)) + m2 = Matrix([6, 5, 4, 3, 2, 1], (3, 2)) actual = m1 * m2 - expected = Matrix([11], (1, 1)) + expected = Matrix([20, 14, 56, 41], (2, 2)) + + self.assertEqual(expected, actual) + + def test_should_mul_matrices_3(self): + m1 = Matrix([1, 2, 3, 4, 5, 6], (3, 2)) + m2 = Matrix([6, 5, 4, 3, 2, 1], (2, 3)) + + actual = m1 * m2 + expected = Matrix([12, 9, 6, 30, 23, 16, 48, 37, 26], (3, 3)) self.assertEqual(expected, actual) @@ -236,15 +254,6 @@ class TestMatrix(TestCase): self.assertEqual(expected, actual) - def test_should_div_matrix_by_scalar(self): - m = Matrix([5, 10, 15, 20], (2, 2)) - s = 5 - - actual = m / s - expected = Matrix([1, 2, 3, 4], (2, 2)) - - self.assertEqual(expected, actual) - def test_should_return_frobenius_norm(self): m = Matrix([1, 2, 3, 4], (2, 2)) @@ -256,7 +265,7 @@ class TestMatrix(TestCase): def test_should_return_colsum_norm(self): m = Matrix([1, 2, 3, 4], (2, 2)) - actual = m.norm("colsum") + actual = m.norm("col sum") expected = 6 self.assertEqual(expected, actual) @@ -264,7 +273,7 @@ class TestMatrix(TestCase): def test_should_return_rowsum_norm(self): m = Matrix([1, 2, 3, 4], (2, 2)) - actual = m.norm("rowsum") + actual = m.norm("row sum") expected = 7 self.assertEqual(expected, actual) @@ -289,7 +298,7 @@ class TestMatrix(TestCase): m = Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9], (3, 3)) actual = m[0] - expected = Matrix([1, 2, 3], (1, 3)) + expected = [1, 2, 3] self.assertEqual(expected, actual) @@ -297,7 +306,7 @@ class TestMatrix(TestCase): m = Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9], (3, 3)) actual = m[2, 0:2] - expected = Matrix([7, 8], (1, 2)) + expected = [7, 8] self.assertEqual(expected, actual) @@ -305,7 +314,7 @@ class TestMatrix(TestCase): m = Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9], (3, 3)) actual = m[:, 1] - expected = Matrix([2, 5, 8], (1, 3)) + expected = [2, 5, 8] self.assertEqual(expected, actual) @@ -313,7 +322,7 @@ class TestMatrix(TestCase): m = Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9], (3, 3)) actual = m[[0, 2], 0] - expected = Matrix([1, 7], (1, 2)) + expected = [1, 7] self.assertEqual(expected, actual) diff --git a/test/test_serial.py b/test/test_serial.py index f657095..d9c043b 100644 --- a/test/test_serial.py +++ b/test/test_serial.py @@ -125,9 +125,9 @@ print("\n\nTesting the matrix class -----------------------------------\n\n") print("Start 2a initialization") a_list = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 2 * np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])]) A = Matrix(a_list) -B = Matrix(structure="tridiagonal", given_size=11) +B = Matrix([-1, 2, -1], structure="tridiagonal", n=11) c_list = [[(i + 1) / (index + 1) for index in range(10)] for i in range(10)] -C = Matrix(c_list, given_shape=(10, 10)) +C = Matrix(c_list, shape=(10, 10)) print("End 2a\n") ### 1b __str__ function, string representation @@ -141,7 +141,7 @@ print("End 2b\n") ### 1c shape and transpose print("Start 2c shape and transpose") # Initialization -A = Matrix(np.array([i for i in range(12)]).reshape(-1, 1), given_shape=(4, 3)) +A = Matrix(np.array([i for i in range(12)]).reshape(-1, 1), shape=(4, 3)) print(f"A has shape {A.shape()} | must be (4,3)") print(f"A.T() has shape {A.T().shape()} | must be (3,4)") print(f"A.T().T() has shape {A.T().T().shape()} | must be (4,3)") @@ -150,17 +150,17 @@ print("End 2c\n") ### 1d addition and substraction print("Start 2d addition and substraction") # Initialization -A = Matrix(structure="diagonal", given_values=[3], offset=0, given_size=10) +A = Matrix(structure="diagonal", data=[3], offset=0, n=10) print(str(A)) -A21 = Matrix(structure="diagonal", given_values=[-1], offset=-1, given_size=10) +A21 = Matrix(structure="diagonal", data=[-1], offset=-1, n=10) print(str(A21)) -A12 = Matrix(structure="diagonal", given_values=[-1], offset=+1, given_size=10) +A12 = Matrix(structure="diagonal", data=[-1], offset=+1, n=10) print(str(A12)) -B = Matrix(structure="diagonal", given_values=[1], offset=0, given_size=10) +B = Matrix(structure="diagonal", data=[1], offset=0, n=10) print(str(B)) # computation C = A + A21 + A12 - B -print(str(C) + f"must be\n{Matrix(structure='tridiagonal', given_values=[-1, 2, -1], given_size=10)}") +print(str(C) + f"must be\n{Matrix(structure='tridiagonal', data=[-1, 2, -1], n=10)}") print(str(5 + A - 3)) print("End 2d\n") @@ -195,21 +195,21 @@ print("End 2f\n") ### 1g norm print("Start 2g norm") -A = Matrix(structure="tridiagonal", given_size=50, given_values=[-1, 2, -1]) +A = Matrix(structure="tridiagonal", n=50, data=[-1, 2, -1]) print(f"Frobenius norm of tridiagonal matrix: {A.norm('frobenius')} | must be 17.263") -print(f"Row sum norm of tridiagonal matrix: {A.norm('row sum')} | must be 2") -print(f"Col sum norm of tridiagonal matrix: {A.norm('col sum')} | must be 2") +print(f"Row sum norm of tridiagonal matrix: {A.norm('row sum')} | must be 4") +print(f"Col sum norm of tridiagonal matrix: {A.norm('col sum')} | must be 4") print("End 2g\n") ### 1h negation print("Start 2h negation") -A = Matrix(structure="tridiagonal", given_size=50, given_values=[-1, 2, 1]) +A = Matrix(structure="tridiagonal", n=50, data=[-1, 2, 1]) print(f"Norm of (A + (-A)) is {(A + (-A)).norm('frobenius')} | must be < 1e-8") print("End 2h\n") ### 1i manipulation print("Start 2i manipulation") -A = Matrix(structure="tridiagonal", given_size=10, given_values=[-1, 2, 1]) +A = Matrix(structure="tridiagonal", n=10, data=[-1, 2, 1]) A[1, 1] = 4 A[[1, 2, 3], 2] = [-5, -10, 100] print(str(A))