From 8c2986026bc42d81a6d9386c9fe621fea8ff2d15 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 5 Apr 2021 19:07:38 +0530 Subject: [PATCH] fix(mypy): type annotations for linear algebra algorithms (#4317) * fix(mypy): type annotations for linear algebra algorithms * refactor: remove linear algebra directory from mypy exclude --- .github/workflows/build.yml | 2 +- linear_algebra/src/conjugate_gradient.py | 17 +++-- linear_algebra/src/lib.py | 83 ++++++++++++++--------- linear_algebra/src/polynom_for_points.py | 13 ++-- linear_algebra/src/power_iteration.py | 7 +- linear_algebra/src/rayleigh_quotient.py | 10 ++- linear_algebra/src/test_linear_algebra.py | 40 +++++------ linear_algebra/src/transformations_2d.py | 2 - 8 files changed, 100 insertions(+), 74 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb54d85ea..ca3e80922 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: python -m pip install mypy pytest-cov -r requirements.txt # FIXME: #4052 fix mypy errors in the exclude directories and remove them below - run: mypy --ignore-missing-imports - --exclude '(data_structures|digital_image_processing|dynamic_programming|graphs|linear_algebra|maths|matrix|other|project_euler|scripts|searches|strings*)/$' . + --exclude '(data_structures|digital_image_processing|dynamic_programming|graphs|maths|matrix|other|project_euler|scripts|searches|strings*)/$' . - name: Run tests run: pytest --doctest-modules --ignore=project_euler/ --ignore=scripts/ --cov-report=term-missing:skip-covered --cov=. . - if: ${{ success() }} diff --git a/linear_algebra/src/conjugate_gradient.py b/linear_algebra/src/conjugate_gradient.py index 1a65b8ccf..418ae88a5 100644 --- a/linear_algebra/src/conjugate_gradient.py +++ b/linear_algebra/src/conjugate_gradient.py @@ -3,10 +3,12 @@ Resources: - https://en.wikipedia.org/wiki/Conjugate_gradient_method - https://en.wikipedia.org/wiki/Definite_symmetric_matrix """ +from typing import Any + import numpy as np -def _is_matrix_spd(matrix: np.array) -> bool: +def _is_matrix_spd(matrix: np.ndarray) -> bool: """ Returns True if input matrix is symmetric positive definite. Returns False otherwise. @@ -38,10 +40,11 @@ def _is_matrix_spd(matrix: np.array) -> bool: eigen_values, _ = np.linalg.eigh(matrix) # Check sign of all eigenvalues. - return np.all(eigen_values > 0) + # np.all returns a value of type np.bool_ + return bool(np.all(eigen_values > 0)) -def _create_spd_matrix(dimension: np.int64) -> np.array: +def _create_spd_matrix(dimension: int) -> Any: """ Returns a symmetric positive definite matrix given a dimension. @@ -64,11 +67,11 @@ def _create_spd_matrix(dimension: np.int64) -> np.array: def conjugate_gradient( - spd_matrix: np.array, - load_vector: np.array, + spd_matrix: np.ndarray, + load_vector: np.ndarray, max_iterations: int = 1000, tol: float = 1e-8, -) -> np.array: +) -> Any: """ Returns solution to the linear system np.dot(spd_matrix, x) = b. @@ -141,6 +144,8 @@ def conjugate_gradient( # Update number of iterations. iterations += 1 + if iterations > max_iterations: + break return x diff --git a/linear_algebra/src/lib.py b/linear_algebra/src/lib.py index 353c83340..5e2f82018 100644 --- a/linear_algebra/src/lib.py +++ b/linear_algebra/src/lib.py @@ -22,6 +22,7 @@ Overview: import math import random +from typing import Collection, Optional, Union, overload class Vector: @@ -45,7 +46,7 @@ class Vector: TODO: compare-operator """ - def __init__(self, components=None): + def __init__(self, components: Optional[Collection[float]] = None) -> None: """ input: components or nothing simple constructor for init the vector @@ -54,7 +55,7 @@ class Vector: components = [] self.__components = list(components) - def set(self, components): + def set(self, components: Collection[float]) -> None: """ input: new components changes the components of the vector. @@ -65,13 +66,13 @@ class Vector: else: raise Exception("please give any vector") - def __str__(self): + def __str__(self) -> str: """ returns a string representation of the vector """ return "(" + ",".join(map(str, self.__components)) + ")" - def component(self, i): + def component(self, i: int) -> float: """ input: index (start at 0) output: the i-th component of the vector. @@ -81,22 +82,22 @@ class Vector: else: raise Exception("index out of range") - def __len__(self): + def __len__(self) -> int: """ returns the size of the vector """ return len(self.__components) - def euclidLength(self): + def euclidLength(self) -> float: """ returns the euclidean length of the vector """ - summe = 0 + summe: float = 0 for c in self.__components: summe += c ** 2 return math.sqrt(summe) - def __add__(self, other): + def __add__(self, other: "Vector") -> "Vector": """ input: other vector assumes: other vector has the same size @@ -109,7 +110,7 @@ class Vector: else: raise Exception("must have the same size") - def __sub__(self, other): + def __sub__(self, other: "Vector") -> "Vector": """ input: other vector assumes: other vector has the same size @@ -122,7 +123,15 @@ class Vector: else: # error case raise Exception("must have the same size") - def __mul__(self, other): + @overload + def __mul__(self, other: float) -> "Vector": + ... + + @overload + def __mul__(self, other: "Vector") -> float: + ... + + def __mul__(self, other: Union[float, "Vector"]) -> Union[float, "Vector"]: """ mul implements the scalar multiplication and the dot-product @@ -132,20 +141,20 @@ class Vector: return Vector(ans) elif isinstance(other, Vector) and (len(self) == len(other)): size = len(self) - summe = 0 + summe: float = 0 for i in range(size): summe += self.__components[i] * other.component(i) return summe else: # error case raise Exception("invalid operand!") - def copy(self): + def copy(self) -> "Vector": """ copies this vector and returns it. """ return Vector(self.__components) - def changeComponent(self, pos, value): + def changeComponent(self, pos: int, value: float) -> None: """ input: an index (pos) and a value changes the specified component (pos) with the @@ -156,7 +165,7 @@ class Vector: self.__components[pos] = value -def zeroVector(dimension): +def zeroVector(dimension: int) -> Vector: """ returns a zero-vector of size 'dimension' """ @@ -165,7 +174,7 @@ def zeroVector(dimension): return Vector([0] * dimension) -def unitBasisVector(dimension, pos): +def unitBasisVector(dimension: int, pos: int) -> Vector: """ returns a unit basis vector with a One at index 'pos' (indexing at 0) @@ -177,7 +186,7 @@ def unitBasisVector(dimension, pos): return Vector(ans) -def axpy(scalar, x, y): +def axpy(scalar: float, x: Vector, y: Vector) -> Vector: """ input: a 'scalar' and two vectors 'x' and 'y' output: a vector @@ -192,7 +201,7 @@ def axpy(scalar, x, y): return x * scalar + y -def randomVector(N, a, b): +def randomVector(N: int, a: int, b: int) -> Vector: """ input: size (N) of the vector. random range (a,b) @@ -200,7 +209,7 @@ def randomVector(N, a, b): random integer components between 'a' and 'b'. """ random.seed(None) - ans = [random.randint(a, b) for i in range(N)] + ans = [random.randint(a, b) for _ in range(N)] return Vector(ans) @@ -222,7 +231,7 @@ class Matrix: operator - _ implements the matrix-subtraction """ - def __init__(self, matrix, w, h): + def __init__(self, matrix: list[list[float]], w: int, h: int) -> None: """ simple constructor for initializing the matrix with components. @@ -231,7 +240,7 @@ class Matrix: self.__width = w self.__height = h - def __str__(self): + def __str__(self) -> str: """ returns a string representation of this matrix. @@ -246,7 +255,7 @@ class Matrix: ans += str(self.__matrix[i][j]) + "|\n" return ans - def changeComponent(self, x, y, value): + def changeComponent(self, x: int, y: int, value: float) -> None: """ changes the x-y component of this matrix """ @@ -255,7 +264,7 @@ class Matrix: else: raise Exception("changeComponent: indices out of bounds") - def component(self, x, y): + def component(self, x: int, y: int) -> float: """ returns the specified (x,y) component """ @@ -264,13 +273,13 @@ class Matrix: else: raise Exception("changeComponent: indices out of bounds") - def width(self): + def width(self) -> int: """ getter for the width """ return self.__width - def height(self): + def height(self) -> int: """ getter for the height """ @@ -303,7 +312,15 @@ class Matrix: else: raise Exception("matrix is not square") - def __mul__(self, other): + @overload + def __mul__(self, other: float) -> "Matrix": + ... + + @overload + def __mul__(self, other: Vector) -> Vector: + ... + + def __mul__(self, other: Union[float, Vector]) -> Union[Vector, "Matrix"]: """ implements the matrix-vector multiplication. implements the matrix-scalar multiplication @@ -312,7 +329,7 @@ class Matrix: if len(other) == self.__width: ans = zeroVector(self.__height) for i in range(self.__height): - summe = 0 + summe: float = 0 for j in range(self.__width): summe += other.component(j) * self.__matrix[i][j] ans.changeComponent(i, summe) @@ -330,7 +347,7 @@ class Matrix: ] return Matrix(matrix, self.__width, self.__height) - def __add__(self, other): + def __add__(self, other: "Matrix") -> "Matrix": """ implements the matrix-addition. """ @@ -345,7 +362,7 @@ class Matrix: else: raise Exception("matrix must have the same dimension!") - def __sub__(self, other): + def __sub__(self, other: "Matrix") -> "Matrix": """ implements the matrix-subtraction. """ @@ -361,19 +378,21 @@ class Matrix: raise Exception("matrix must have the same dimension!") -def squareZeroMatrix(N): +def squareZeroMatrix(N: int) -> Matrix: """ returns a square zero-matrix of dimension NxN """ - ans = [[0] * N for i in range(N)] + ans: list[list[float]] = [[0] * N for _ in range(N)] return Matrix(ans, N, N) -def randomMatrix(W, H, a, b): +def randomMatrix(W: int, H: int, a: int, b: int) -> Matrix: """ returns a random matrix WxH with integer components between 'a' and 'b' """ random.seed(None) - matrix = [[random.randint(a, b) for j in range(W)] for i in range(H)] + matrix: list[list[float]] = [ + [random.randint(a, b) for _ in range(W)] for _ in range(H) + ] return Matrix(matrix, W, H) diff --git a/linear_algebra/src/polynom_for_points.py b/linear_algebra/src/polynom_for_points.py index 7a363723d..091849542 100644 --- a/linear_algebra/src/polynom_for_points.py +++ b/linear_algebra/src/polynom_for_points.py @@ -1,6 +1,3 @@ -from __future__ import annotations - - def points_to_polynomial(coordinates: list[list[int]]) -> str: """ coordinates is a two dimensional matrix: [[x, y], [x, y], ...] @@ -55,12 +52,12 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str: if check == 1: count_of_line = 0 - matrix = [] + matrix: list[list[float]] = [] # put the x and x to the power values in a matrix while count_of_line < x: count_in_line = 0 a = coordinates[count_of_line][0] - count_line: list[int] = [] + count_line: list[float] = [] while count_in_line < x: count_line.append(a ** (x - (count_in_line + 1))) count_in_line += 1 @@ -69,7 +66,7 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str: count_of_line = 0 # put the y values into a vector - vector: list[int] = [] + vector: list[float] = [] while count_of_line < x: vector.append(coordinates[count_of_line][1]) count_of_line += 1 @@ -96,14 +93,14 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str: # make solutions solution: list[str] = [] while count < x: - solution.append(vector[count] / matrix[count][count]) + solution.append(str(vector[count] / matrix[count][count])) count += 1 count = 0 solved = "f(x)=" while count < x: - remove_e: list[str] = str(solution[count]).split("E") + remove_e: list[str] = solution[count].split("E") if len(remove_e) > 1: solution[count] = remove_e[0] + "*10^" + remove_e[1] solved += "x^" + str(x - (count + 1)) + "*" + str(solution[count]) diff --git a/linear_algebra/src/power_iteration.py b/linear_algebra/src/power_iteration.py index 476361e0d..2cf22838e 100644 --- a/linear_algebra/src/power_iteration.py +++ b/linear_algebra/src/power_iteration.py @@ -2,8 +2,11 @@ import numpy as np def power_iteration( - input_matrix: np.array, vector: np.array, error_tol=1e-12, max_iterations=100 -) -> [float, np.array]: + input_matrix: np.ndarray, + vector: np.ndarray, + error_tol: float = 1e-12, + max_iterations: int = 100, +) -> tuple[float, np.ndarray]: """ Power Iteration. Find the largest eignevalue and corresponding eigenvector diff --git a/linear_algebra/src/rayleigh_quotient.py b/linear_algebra/src/rayleigh_quotient.py index 69bbbac11..78083aa75 100644 --- a/linear_algebra/src/rayleigh_quotient.py +++ b/linear_algebra/src/rayleigh_quotient.py @@ -1,10 +1,12 @@ """ https://en.wikipedia.org/wiki/Rayleigh_quotient """ +from typing import Any + import numpy as np -def is_hermitian(matrix: np.array) -> bool: +def is_hermitian(matrix: np.ndarray) -> bool: """ Checks if a matrix is Hermitian. >>> import numpy as np @@ -24,7 +26,7 @@ def is_hermitian(matrix: np.array) -> bool: return np.array_equal(matrix, matrix.conjugate().T) -def rayleigh_quotient(A: np.array, v: np.array) -> float: +def rayleigh_quotient(A: np.ndarray, v: np.ndarray) -> Any: """ Returns the Rayleigh quotient of a Hermitian matrix A and vector v. @@ -43,7 +45,9 @@ def rayleigh_quotient(A: np.array, v: np.array) -> float: array([[3.]]) """ v_star = v.conjugate().T - return (v_star.dot(A).dot(v)) / (v_star.dot(v)) + v_star_dot = v_star.dot(A) + assert isinstance(v_star_dot, np.ndarray) + return (v_star_dot.dot(v)) / (v_star.dot(v)) def tests() -> None: diff --git a/linear_algebra/src/test_linear_algebra.py b/linear_algebra/src/test_linear_algebra.py index 6eba3a163..0954a2d93 100644 --- a/linear_algebra/src/test_linear_algebra.py +++ b/linear_algebra/src/test_linear_algebra.py @@ -12,7 +12,7 @@ from .lib import Matrix, Vector, axpy, squareZeroMatrix, unitBasisVector, zeroVe class Test(unittest.TestCase): - def test_component(self): + def test_component(self) -> None: """ test for method component """ @@ -21,28 +21,28 @@ class Test(unittest.TestCase): self.assertEqual(x.component(2), 3) _ = Vector() - def test_str(self): + def test_str(self) -> None: """ test for toString() method """ x = Vector([0, 0, 0, 0, 0, 1]) self.assertEqual(str(x), "(0,0,0,0,0,1)") - def test_size(self): + def test_size(self) -> None: """ test for size()-method """ x = Vector([1, 2, 3, 4]) self.assertEqual(len(x), 4) - def test_euclidLength(self): + def test_euclidLength(self) -> None: """ test for the eulidean length """ x = Vector([1, 2]) self.assertAlmostEqual(x.euclidLength(), 2.236, 3) - def test_add(self): + def test_add(self) -> None: """ test for + operator """ @@ -52,7 +52,7 @@ class Test(unittest.TestCase): self.assertEqual((x + y).component(1), 3) self.assertEqual((x + y).component(2), 4) - def test_sub(self): + def test_sub(self) -> None: """ test for - operator """ @@ -62,7 +62,7 @@ class Test(unittest.TestCase): self.assertEqual((x - y).component(1), 1) self.assertEqual((x - y).component(2), 2) - def test_mul(self): + def test_mul(self) -> None: """ test for * operator """ @@ -72,19 +72,19 @@ class Test(unittest.TestCase): self.assertEqual(str(x * 3.0), "(3.0,6.0,9.0)") self.assertEqual((a * b), 0) - def test_zeroVector(self): + def test_zeroVector(self) -> None: """ test for the global function zeroVector(...) """ self.assertTrue(str(zeroVector(10)).count("0") == 10) - def test_unitBasisVector(self): + def test_unitBasisVector(self) -> None: """ test for the global function unitBasisVector(...) """ self.assertEqual(str(unitBasisVector(3, 1)), "(0,1,0)") - def test_axpy(self): + def test_axpy(self) -> None: """ test for the global function axpy(...) (operation) """ @@ -92,7 +92,7 @@ class Test(unittest.TestCase): y = Vector([1, 0, 1]) self.assertEqual(str(axpy(2, x, y)), "(3,4,7)") - def test_copy(self): + def test_copy(self) -> None: """ test for the copy()-method """ @@ -100,7 +100,7 @@ class Test(unittest.TestCase): y = x.copy() self.assertEqual(str(x), str(y)) - def test_changeComponent(self): + def test_changeComponent(self) -> None: """ test for the changeComponent(...)-method """ @@ -109,43 +109,43 @@ class Test(unittest.TestCase): x.changeComponent(1, 1) self.assertEqual(str(x), "(0,1,0)") - def test_str_matrix(self): + def test_str_matrix(self) -> None: A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) self.assertEqual("|1,2,3|\n|2,4,5|\n|6,7,8|\n", str(A)) - def test_determinate(self): + def test_determinate(self) -> None: """ test for determinate() """ A = Matrix([[1, 1, 4, 5], [3, 3, 3, 2], [5, 1, 9, 0], [9, 7, 7, 9]], 4, 4) self.assertEqual(-376, A.determinate()) - def test__mul__matrix(self): + def test__mul__matrix(self) -> None: A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 3, 3) x = Vector([1, 2, 3]) self.assertEqual("(14,32,50)", str(A * x)) self.assertEqual("|2,4,6|\n|8,10,12|\n|14,16,18|\n", str(A * 2)) - def test_changeComponent_matrix(self): + def test_changeComponent_matrix(self) -> None: A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) A.changeComponent(0, 2, 5) self.assertEqual("|1,2,5|\n|2,4,5|\n|6,7,8|\n", str(A)) - def test_component_matrix(self): + def test_component_matrix(self) -> None: A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) self.assertEqual(7, A.component(2, 1), 0.01) - def test__add__matrix(self): + def test__add__matrix(self) -> None: A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) B = Matrix([[1, 2, 7], [2, 4, 5], [6, 7, 10]], 3, 3) self.assertEqual("|2,4,10|\n|4,8,10|\n|12,14,18|\n", str(A + B)) - def test__sub__matrix(self): + def test__sub__matrix(self) -> None: A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) B = Matrix([[1, 2, 7], [2, 4, 5], [6, 7, 10]], 3, 3) self.assertEqual("|0,0,-4|\n|0,0,0|\n|0,0,-2|\n", str(A - B)) - def test_squareZeroMatrix(self): + def test_squareZeroMatrix(self) -> None: self.assertEqual( "|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|" + "\n|0,0,0,0,0|\n", str(squareZeroMatrix(5)), diff --git a/linear_algebra/src/transformations_2d.py b/linear_algebra/src/transformations_2d.py index 6a15189c5..cdf42100d 100644 --- a/linear_algebra/src/transformations_2d.py +++ b/linear_algebra/src/transformations_2d.py @@ -11,8 +11,6 @@ projection(45) = [[0.27596319193541496, 0.446998331800279], reflection(45) = [[0.05064397763545947, 0.893996663600558], [0.893996663600558, 0.7018070490682369]] """ -from __future__ import annotations - from math import cos, sin