From 3ec62d99f7f8aca5720420230b221c8b5d0617f1 Mon Sep 17 00:00:00 2001 From: Niklas Birk Date: Thu, 22 Feb 2024 02:21:05 +0100 Subject: [PATCH] Finish vector.py; Adjust matrix.py for use of vector.py as subclass; Adjust vector.py and matrix.py to match test_serial.py --- src/matrix.py | 32 ++++++----- src/vector.py | 116 +++++++++++++++++++++++++++++++++++---- test/test_serial.py | 9 +-- test/test_vector.py | 131 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 249 insertions(+), 39 deletions(-) diff --git a/src/matrix.py b/src/matrix.py index 5397452..c0f8acd 100644 --- a/src/matrix.py +++ b/src/matrix.py @@ -92,20 +92,21 @@ class Matrix: """ return self.__shape__ + def __transpose_internal__(self): + 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 transposed_data, (cols, rows) + def transpose(self): """ :return: the transpose of the matrix """ - 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)) + transposed_data, shape = self.__transpose_internal__() + return Matrix(transposed_data, shape) def T(self): """ @@ -123,9 +124,9 @@ 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): - data_to_compare = other.__data__ if self.__shape__ != other.__shape__: return False + data_to_compare = other.__data__ elif isinstance(other, list): data_to_compare = other if self.__shape__[0] != len(other) or self.__shape__[1] != len(other[0]): @@ -144,10 +145,13 @@ class Matrix: def __str__(self): return str(numpy.array(self.__data__)) - def __neg__(self): + def __neg_internal__(self): 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__) + return [[-(self.__data__[i][j]) for j in cols] for i in rows] + + def __neg__(self): + return Matrix(self.__neg_internal__(), self.__shape__) def __add_matrix_internal__(self, other): rows = self.__shape__[0] @@ -201,8 +205,8 @@ class Matrix: return new_data def __mul_scalar_internal__(self, other): - cols = range(self.__shape__[1]) rows = range(self.__shape__[0]) + cols = range(self.__shape__[1]) return [[(self.__data__[i][j] * other) for j in cols] for i in rows] def __mul__(self, other): diff --git a/src/vector.py b/src/vector.py index c9403df..c8bf6a8 100644 --- a/src/vector.py +++ b/src/vector.py @@ -4,39 +4,118 @@ from matrix import Matrix class Vector(Matrix): - def __init__(self, data): + def __init__(self, data=None, shape=None): """ :type data: numpy.ndarray | list | int + :type shape: (int, int) """ + if shape is None and not isinstance(data, int): + shape = (len(data), 1) + if isinstance(data, numpy.ndarray): - super().__init__(data) + self.__init__(data.tolist()) elif isinstance(data, list): - super().__init__(data, (len(data), 1)) + if len(data) == 1 and isinstance(data[0], list): + shape = (1, len(data[0])) + super().__init__(data, shape) elif isinstance(data, int): self.__init__([0] * data) else: - raise ValueError("data must be a list or an integer for dimension") + raise ValueError("data must be a ``list``, a ``numpy.ndarray`` or an integer for dimension") - def get_dimension(self): - return super().shape()[0] + def __eq__(self, other): + """ + Return ``self==value`` + + :param other: The object to compare to; must be either a ``Vector``, a ``list`` or a ``numpy.ndarray`` + :return: True if data in the same-shaped vectors are equal to the given data in other for each component otherwise False + """ + if isinstance(other, Vector): + if self.__shape__ != other.__shape__: + return False + data_to_compare = numpy.array(other.__data__).flatten().tolist() + elif isinstance(other, list): + data_to_compare = numpy.array(other).flatten().tolist() + elif isinstance(other, numpy.ndarray): + data_to_compare = other.flatten().tolist() + else: + raise ValueError("Vector type is not comparable to type of given ``other``") + return numpy.array_equal(data_to_compare, numpy.array(self.__data__).flatten().tolist()) def transpose(self): - return Vector(self.__data__.reshape(self.__shape__[1], self.__shape__[0])) + """ + :return: the transpose of the vector + """ + transposed_data, shape = super().__transpose_internal__() + return Vector(transposed_data, shape) + + def T(self): + return self.transpose() + + def __neg__(self): + return Vector(super().__neg_internal__(), self.__shape__) + + def __add__(self, other): + if isinstance(other, Vector): + if self.__shape__ != other.__shape__: + raise ValueError("The shape of the operands must be the same") + return Vector(super().__add_matrix_internal__(other), self.__shape__) + elif isinstance(other, int) or isinstance(other, float): + return Vector(super().__add_scalar_internal__(other), self.__shape__) + else: + raise ValueError("Only a number or another ``Vector`` can be added to a ``Vector``") + + def __mul_vector_same_shape_internal__(self, other): + rows = self.__shape__[0] + cols = self.__shape__[1] + if rows >= cols: + new_data = [(self.__data__[i][0] * other.__data__[i][0]) for i in range(rows)] + else: + new_data = [(self.__data__[0][j] * other.__data__[0][j]) for j in range(cols)] + return new_data + + def __mul_tensor_internal__(self, other): + rows = self.__shape__[0] + cols = other.__shape__[1] + return [[self.__data__[i][0] * other.__data__[0][j] for j in range(cols)] for i in range(rows)], (rows, cols) def __mul__(self, other): if isinstance(other, Vector): - if self.shape() == other.shape(): - return Vector(self.__data__ * other.__data__) - return super().__mul__(other)[0][0] + if self.__shape__ == other.__shape__: + return Vector(self.__mul_vector_same_shape_internal__(other)) + elif self.__shape__ == tuple(reversed(other.__shape__)): + if self.__shape__[0] == 1: # Case (_ ... _) * (_\n...\n_) = scalar + return super().__mul_matrix_internal__(other)[0][0] + else: # Case (_\n...\n_) * (_ ... _) = Matrix + new_data, shape = self.__mul_tensor_internal__(other) + return Matrix(new_data, shape) + else: + raise ValueError("The shapes of the operands must be the compatible") elif isinstance(other, int) or isinstance(other, float): - return Vector(super().__mul__(other).__data__) + return Vector(super().__mul_scalar_internal__(other)) else: - raise ValueError("A vector can only be multiplied with an vector (dot product) or a scalar") + raise ValueError("A ``Vector`` can only be multiplied with an ``Vector`` (dot product or tensor)," + "a compatible ``Matrix`` or a scalar") def __rmul__(self, other): return self * other + def __truediv_vector_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 __truediv__(self, other): + if isinstance(other, Vector): + if self.__shape__ != other.__shape__: + raise ValueError("The ``Vector``s to be divided must have the same shape") + return Vector(self.__truediv_vector_internal__(other)) + elif isinstance(other, int) or isinstance(other, float): + return Vector(super().__truediv_scalar_internal__(other)) + else: + raise ValueError("A ``Vector`` can only be divided ba a number or another same-shaped ``Vector``") + def norm(self, **kwargs): """ Computes the 2-norm of the vector which is the Frobenius-Norm of a nx1 matrix. @@ -54,3 +133,16 @@ class Vector(Matrix): :return: the normalized vector """ return self / self.norm() + + def __getitem__(self, key): + if isinstance(key, tuple): + return numpy.array(self.__data__).flatten()[[key]].tolist() + return numpy.array(self.__data__).flatten()[key].tolist() + + def __setitem__(self, key, value): + manipulated_data = numpy.array(self.__data__).flatten() + if isinstance(key, tuple): + manipulated_data[[key]] = value + else: + manipulated_data[key] = value + self.__data__ = Vector(manipulated_data.tolist(), self.__shape__).__data__ diff --git a/test/test_serial.py b/test/test_serial.py index d9c043b..8d7d5bd 100644 --- a/test/test_serial.py +++ b/test/test_serial.py @@ -58,7 +58,7 @@ else: print("It is required to raise a system error, e. g., ValueError, since dimensions mismatch!") print("End 1d\n") -### 1e multiplication +### 1e multiplication print("Start 1e multiplication") # intitialization a = Vector([1, 3, 5, 7, 9]) @@ -77,7 +77,7 @@ y = 0.1 * b.T() print(f"0.1 * b.T()= {str(y)} | must be {0.1 * np.array([-2, 5, 1, 0, -3])}") print("End 1e\n") -### 1f divison +### 1f divison print("Start 1f divison") # intitialization a = Vector([1, 3, 5, 7, 9]) @@ -93,7 +93,8 @@ print("End 1f\n") print("Start 1g norm") # intitialization a = Vector([1, 3, 5, 7, 9]) -a_norm, a_normalized = a.normalize() +a_norm = a.norm() +a_normalized = a.normalize() print(f"a_norm = {a_norm} | must be {np.linalg.norm([1, 3, 5, 7, 9])}") print(f"a_normalize = {str(a_normalized)} | must be {np.array([1, 3, 5, 7, 9]) / np.linalg.norm([1, 3, 5, 7, 9])}") print("End 1g\n") @@ -111,7 +112,7 @@ print("Start 1i manipulation") # intitialization a = Vector([1, 3, 5, 7, 9]) print( - f"a[{str([1, 2, 4])}] = {str(a[1, 2, 4].reshape(3, ))} | must be {np.array([1, 3, 5, 7, 9]).reshape(5, 1)[np.array([1, 2, 4])].reshape(3, )}") + f"a[{str([1, 2, 4])}] = {str(np.array(a[1, 2, 4]).reshape(3, ))} | must be {np.array([1, 3, 5, 7, 9]).reshape(5, 1)[np.array([1, 2, 4])].reshape(3, )}") a[1, 2, 4] = [-1, -1, -1] print(f"a = {str(a)} | must be {np.array([1, -1, -1, 5, 7, -1])}") print("End 1i\n") diff --git a/test/test_vector.py b/test/test_vector.py index 7b2cc5c..5ca714f 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -4,12 +4,6 @@ from vector import Vector class TestVector(TestCase): - def test_should_create_vector_dim_5(self): - actual = Vector(5).get_dimension() - expected = 5 - - self.assertEqual(expected, actual) - def test_should_create_zero_vector(self): actual = Vector(5) expected = Vector([0, 0, 0, 0, 0]) @@ -22,7 +16,7 @@ class TestVector(TestCase): self.assertEqual(expected, actual) - def test_should_transpose_vector(self): + def test_should_transpose_col_vector(self): data = [1, 2, 3, 4, 5, 6] actual = Vector(data) @@ -30,6 +24,14 @@ class TestVector(TestCase): expected = [[1, 2, 3, 4, 5, 6]] self.assertEqual(expected, actual) + def test_should_transpose_row_vector(self): + data = [[1, 2, 3, 4, 5, 6]] + actual = Vector(data) + + actual = actual.transpose() + expected = [[1], [2], [3], [4], [5], [6]] + self.assertEqual(expected, actual) + def test_should_neg_vector(self): v = Vector([1, 2]) @@ -104,6 +106,24 @@ class TestVector(TestCase): self.assertEqual(expected, actual) + def test_should_truediv_scalar(self): + v = Vector([1, 2]) + s = 5 + + expected = Vector([1/5, 2/5]) + actual = v / s + + self.assertEqual(expected, actual) + + def test_should_truediv_same_shape_vectors(self): + v1 = Vector([1, 2]) + v2 = Vector([3, 4]) + + expected = Vector([1/3, 1/2]) + actual = v1 / v2 + + self.assertEqual(expected, actual) + def test_should_mul_same_shape_vectors(self): v1 = Vector([1, 2]) v2 = Vector([3, 4]) @@ -122,6 +142,15 @@ class TestVector(TestCase): self.assertEqual(expected, actual) + def test_should_mul_vectors_tensor(self): + v1 = Vector([1, 2]) + v2 = Vector([3, 4]) + + expected = [[3, 4], [6, 8]] + actual = v1 * v2.T() + + self.assertEqual(expected, actual) + def test_should_mul_scalar_with_vector(self): v = Vector([1, 2]) s = 2 @@ -160,5 +189,89 @@ class TestVector(TestCase): actual = v.normalize() expected = [1 / 2.236, 2 / 2.236] - self.assertAlmostEqual(expected[0], actual[0][0], 3) - self.assertAlmostEqual(expected[1], actual[1][0], 3) + self.assertAlmostEqual(expected[0], actual[0], 3) + self.assertAlmostEqual(expected[1], actual[1], 3) + + def test_should_return_first_element_of_column_vector(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + actual = m[0] + expected = 1 + + self.assertEqual(expected, actual) + + def test_should_return_first_element_of_row_vector(self): + m = Vector([[1, 2, 3, 4, 5, 6, 7, 8, 9]]) + + actual = m[0] + expected = 1 + + self.assertEqual(expected, actual) + + def test_should_return_last_element_of_column_vector(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + actual = m[8] + expected = 9 + + self.assertEqual(expected, actual) + + def test_should_return_last_element_of_row_vector(self): + m = Vector([[1, 2, 3, 4, 5, 6, 7, 8, 9]]) + + actual = m[8] + expected = 9 + + self.assertEqual(expected, actual) + + def test_should_return_all_except_last_element(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + actual = m[0:8] + expected = Vector([1, 2, 3, 4, 5, 6, 7, 8]) + + self.assertEqual(expected, actual) + + def test_should_return_all_except_first_and_last_element(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + actual = m[1:8] + expected = Vector([2, 3, 4, 5, 6, 7, 8]) + + self.assertEqual(expected, actual) + + def test_should_return_some_element(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + actual = m[0, 2, 4, 6] + expected = Vector([1, 3, 5, 7]) + + self.assertEqual(expected, actual) + + def test_should_set_first_element(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + m[0] = 10 + actual = m + expected = Vector([10, 2, 3, 4, 5, 6, 7, 8, 9]) + + self.assertEqual(expected, actual) + + def test_should_set_all_except_first_and_last_element(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + m[1:8] = [4, 4, 4, 4, 4, 4, 4] + actual = m + expected = Vector([1, 4, 4, 4, 4, 4, 4, 4, 9]) + + self.assertEqual(expected, actual) + + def test_should_set_some_elements(self): + m = Vector([1, 2, 3, 4, 5, 6, 7, 8, 9]) + + m[0, 2, 4, 6] = [10, 30, 50, 70] + actual = m + expected = Vector([10, 2, 30, 4, 50, 6, 70, 8, 9]) + + self.assertEqual(expected, actual) +