diff --git a/src/matrixmania/__init__.py b/src/matrixmania/__init__.py index 223820d..224ed5f 100644 --- a/src/matrixmania/__init__.py +++ b/src/matrixmania/__init__.py @@ -1 +1,5 @@ -from .compute import matmul, transpose, rot_2d \ No newline at end of file +"""MatrixMania - Paket für Matrixoperationen.""" + +from .compute import matmul, rot_2d, rot_3d, transpose + +__all__ = ["matmul", "rot_2d", "rot_3d", "transpose"] diff --git a/src/matrixmania/compute.py b/src/matrixmania/compute.py index c495284..8e27a0d 100644 --- a/src/matrixmania/compute.py +++ b/src/matrixmania/compute.py @@ -5,7 +5,9 @@ import math from tabulate import tabulate -def matmul(matrix_a: list[list[float]], matrix_b: list[list[float]]) -> list[list[float]]: +def matmul( + matrix_a: list[list[float]], matrix_b: list[list[float]] +) -> list[list[float]]: """Berechnet das Matrixprodukt zweier Matrizen (matrix_a @ matrix_b). :param matrix_a: Die linke Matrix. @@ -21,11 +23,15 @@ def matmul(matrix_a: list[list[float]], matrix_b: list[list[float]]) -> list[lis rows_a, cols_a = len(matrix_a), len(matrix_a[0]) rows_b, cols_b = len(matrix_b), len(matrix_b[0]) - if any(len(row) != cols_a for row in matrix_a) or any(len(row) != cols_b for row in matrix_b): + if any(len(row) != cols_a for row in matrix_a) or any( + len(row) != cols_b for row in matrix_b + ): raise ValueError("Alle Zeilen einer Matrix müssen dieselbe Länge besitzen.") if cols_a != rows_b: - raise ValueError("Matrixdimensionen stimmen für die Multiplikation nicht überein.") + raise ValueError( + "Matrixdimensionen stimmen für die Multiplikation nicht überein." + ) result: list[list[float]] = [] for i in range(rows_a): @@ -78,10 +84,56 @@ def rot_2d(angle: float) -> list[list[float]]: def rot_3d(angle: float, axis: str) -> list[list[float]]: - pass + """Berechnet die 3D-Rotationsmatrix für einen Drehwinkel um eine Achse. + + Die Rotationsmatrizen lauten: + - Für die x-Achse R_x(theta): + [[1, 0, 0], + [0, cos(theta), -sin(theta)], + [0, sin(theta), cos(theta)]] + - Für die y-Achse R_y(theta): + [[cos(theta), 0, sin(theta)], + [0, 1, 0], + [-sin(theta), 0, cos(theta)]] + - Für die z-Achse R_z(theta): + [[cos(theta), -sin(theta), 0], + [sin(theta), cos(theta), 0], + [0, 0, 1]] + + :param angle: Der Rotationswinkel im Bogenmaß (Radiant). + :param axis: Die Rotationsachse ('x', 'y' oder 'z'). + :return: Die 3x3-Rotationsmatrix als verschachtelte Liste. + :raises ValueError: Wenn die Achse nicht 'x', 'y' oder 'z' ist. + """ + if axis not in ("x", "y", "z"): + raise ValueError( + f"Ungültige Achse '{axis}'. Erlaubte Werte sind 'x', 'y' oder 'z'." + ) + + cos_theta = math.cos(angle) + sin_theta = math.sin(angle) + + if axis == "x": + return [ + [1.0, 0.0, 0.0], + [0.0, cos_theta, -sin_theta], + [0.0, sin_theta, cos_theta], + ] + if axis == "y": + return [ + [cos_theta, 0.0, sin_theta], + [0.0, 1.0, 0.0], + [-sin_theta, 0.0, cos_theta], + ] + return [ + [cos_theta, -sin_theta, 0.0], + [sin_theta, cos_theta, 0.0], + [0.0, 0.0, 1.0], + ] -def test_functions(): +def test_functions() -> None: + """Testet die mathematischen Hilfsfunktionen für Matrixoperationen.""" a1 = [ [3, 4, -1, 4], [-2, 2, 5, 1], @@ -97,7 +149,9 @@ def test_functions(): [-1, 27, -8], ] c1 = matmul(a1, b1) - assert c1 == expected_c1, f"Fehler in Beispiel 1: Erwartet {expected_c1}, erhalten {c1}" + assert c1 == expected_c1, ( + f"Fehler in Beispiel 1: Erwartet {expected_c1}, erhalten {c1}" + ) print("Test 1 (Skript-Beispiel) erfolgreich bestanden!") a2 = [ @@ -178,5 +232,40 @@ def test_functions(): assert math.isclose(rotated_point[1][0], 1.0, abs_tol=1e-9) print("Test rot_2D (Vektordrehung mit matmul) erfolgreich bestanden!") + # Test 10: rot_3d um x-Achse um pi / 2 + rot_x_90 = rot_3d(math.pi / 2, "x") + v_y = [[0.0], [1.0], [0.0]] + rotated_v_y = matmul(rot_x_90, v_y) + assert math.isclose(rotated_v_y[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated_v_y[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated_v_y[2][0], 1.0, abs_tol=1e-9) + print("Test rot_3D (x-Achse) erfolgreich bestanden!") + + # Test 11: rot_3d um y-Achse um pi / 2 + rot_y_90 = rot_3d(math.pi / 2, "y") + v_z = [[0.0], [0.0], [1.0]] + rotated_v_z = matmul(rot_y_90, v_z) + assert math.isclose(rotated_v_z[0][0], 1.0, abs_tol=1e-9) + assert math.isclose(rotated_v_z[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated_v_z[2][0], 0.0, abs_tol=1e-9) + print("Test rot_3D (y-Achse) erfolgreich bestanden!") + + # Test 12: rot_3d um z-Achse um pi / 2 + rot_z_90 = rot_3d(math.pi / 2, "z") + v_x = [[1.0], [0.0], [0.0]] + rotated_v_x = matmul(rot_z_90, v_x) + assert math.isclose(rotated_v_x[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated_v_x[1][0], 1.0, abs_tol=1e-9) + assert math.isclose(rotated_v_x[2][0], 0.0, abs_tol=1e-9) + print("Test rot_3D (z-Achse) erfolgreich bestanden!") + + # Test 13: rot_3d mit ungültiger Achse + try: + rot_3d(math.pi / 2, "invalid") + raise AssertionError("Ungültige Achse wurde nicht abgefangen!") + except ValueError: + print("Test 13 (rot_3d ungültige Achse) erfolgreich abgefangen!") + + if __name__ == "__main__": - print(tabulate(rot_2d(1.0))) \ No newline at end of file + print(tabulate(rot_2d(1.0))) diff --git a/test/test_matrixmania/test_compute.py b/test/test_matrixmania/test_compute.py new file mode 100644 index 0000000..d538f8b --- /dev/null +++ b/test/test_matrixmania/test_compute.py @@ -0,0 +1,154 @@ +"""Unit-Tests für die mathematischen Matrixfunktionen in compute.py.""" + +import math + +import pytest + +from matrixmania.compute import matmul, rot_2d, rot_3d, test_functions, transpose + + +def test_matmul_valid() -> None: + """Testet die Matrixmultiplikation mit gültigen Matrizen.""" + a = [ + [3.0, 4.0, -1.0, 4.0], + [-2.0, 2.0, 5.0, 1.0], + ] + b = [ + [1.0, 3.0, -2.0], + [2.0, 5.0, 1.0], + [-1.0, 4.0, -4.0], + [2.0, 3.0, 6.0], + ] + expected = [ + [20.0, 37.0, 26.0], + [-1.0, 27.0, -8.0], + ] + assert matmul(a, b) == expected + + +def test_matmul_invalid_dimensions() -> None: + """Testet, dass bei inkompatiblen Matrixdimensionen ValueError geworfen wird.""" + a = [[1.0, 2.0], [3.0, 4.0]] + b = [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]] + with pytest.raises(ValueError): + matmul(a, b) + + +def test_matmul_empty_or_uneven() -> None: + """Testet matmul mit leeren oder ungleichmäßigen Matrizen.""" + with pytest.raises(ValueError): + matmul([], [[1.0]]) + with pytest.raises(ValueError): + matmul([[1.0, 2.0], [3.0]], [[1.0], [2.0]]) + + +def test_transpose_valid() -> None: + """Testet das Transponieren einer regulären Matrix.""" + matrix = [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + ] + expected = [ + [1.0, 4.0], + [2.0, 5.0], + [3.0, 6.0], + ] + assert transpose(matrix) == expected + + +def test_transpose_invalid() -> None: + """Testet, dass transpose bei leeren oder unregelmäßigen Matrizen Fehler wirft.""" + with pytest.raises(ValueError): + transpose([]) + with pytest.raises(ValueError): + transpose([[1.0, 2.0], [3.0]]) + + +def test_rot_2d() -> None: + """Testet die 2D-Rotation für verschiedene Winkel.""" + rot_0 = rot_2d(0.0) + assert math.isclose(rot_0[0][0], 1.0, abs_tol=1e-9) + assert math.isclose(rot_0[0][1], 0.0, abs_tol=1e-9) + assert math.isclose(rot_0[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(rot_0[1][1], 1.0, abs_tol=1e-9) + + rot_90 = rot_2d(math.pi / 2) + assert math.isclose(rot_90[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(rot_90[0][1], -1.0, abs_tol=1e-9) + assert math.isclose(rot_90[1][0], 1.0, abs_tol=1e-9) + assert math.isclose(rot_90[1][1], 0.0, abs_tol=1e-9) + + +def test_rot_3d_x_axis() -> None: + """Testet die 3D-Rotation um die x-Achse.""" + matrix_x_0 = rot_3d(0.0, "x") + expected_identity = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + for r in range(3): + for c in range(3): + assert math.isclose(matrix_x_0[r][c], expected_identity[r][c], abs_tol=1e-9) + + matrix_x_90 = rot_3d(math.pi / 2, "x") + v_y = [[0.0], [1.0], [0.0]] + rotated = matmul(matrix_x_90, v_y) + assert math.isclose(rotated[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated[2][0], 1.0, abs_tol=1e-9) + + +def test_rot_3d_y_axis() -> None: + """Testet die 3D-Rotation um die y-Achse.""" + matrix_y_0 = rot_3d(0.0, "y") + expected_identity = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + for r in range(3): + for c in range(3): + assert math.isclose(matrix_y_0[r][c], expected_identity[r][c], abs_tol=1e-9) + + matrix_y_90 = rot_3d(math.pi / 2, "y") + v_z = [[0.0], [0.0], [1.0]] + rotated = matmul(matrix_y_90, v_z) + assert math.isclose(rotated[0][0], 1.0, abs_tol=1e-9) + assert math.isclose(rotated[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated[2][0], 0.0, abs_tol=1e-9) + + +def test_rot_3d_z_axis() -> None: + """Testet die 3D-Rotation um die z-Achse.""" + matrix_z_0 = rot_3d(0.0, "z") + expected_identity = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + for r in range(3): + for c in range(3): + assert math.isclose(matrix_z_0[r][c], expected_identity[r][c], abs_tol=1e-9) + + matrix_z_90 = rot_3d(math.pi / 2, "z") + v_x = [[1.0], [0.0], [0.0]] + rotated = matmul(matrix_z_90, v_x) + assert math.isclose(rotated[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(rotated[1][0], 1.0, abs_tol=1e-9) + assert math.isclose(rotated[2][0], 0.0, abs_tol=1e-9) + + +def test_rot_3d_invalid_axis() -> None: + """Testet, dass ungültige Achsen einen ValueError auslösen.""" + with pytest.raises(ValueError, match="Ungültige Achse"): + rot_3d(1.0, "w") + with pytest.raises(ValueError): + rot_3d(0.5, "123") + with pytest.raises(ValueError): + rot_3d(0.0, "") + + +def test_test_functions_execution() -> None: + """Testet die Ausführung der integrierten Testfunktion.""" + test_functions() diff --git a/test/test_matrixmania/test_rot.py b/test/test_matrixmania/test_rot.py new file mode 100644 index 0000000..3c7f8c7 --- /dev/null +++ b/test/test_matrixmania/test_rot.py @@ -0,0 +1,198 @@ +"""Unit-Tests für die Rotationsfunktionen in compute.py.""" + +import math + +import pytest + +from matrixmania import rot_2d, rot_3d + + +def test_rot_2d_zero_angle() -> None: + """Testet die 2D-Rotation um 0 Radiant (Einheitsmatrix).""" + # Arrange + angle = 0.0 + + # Act + result = rot_2d(angle) + + # Assert + assert math.isclose(result[0][0], 1.0, abs_tol=1e-9) + assert math.isclose(result[0][1], 0.0, abs_tol=1e-9) + assert math.isclose(result[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(result[1][1], 1.0, abs_tol=1e-9) + + +def test_rot_2d_pi_half() -> None: + """Testet die 2D-Rotation um pi / 2 Radiant (90 Grad).""" + # Arrange + angle = math.pi / 2 + + # Act + result = rot_2d(angle) + + # Assert + assert math.isclose(result[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(result[0][1], -1.0, abs_tol=1e-9) + assert math.isclose(result[1][0], 1.0, abs_tol=1e-9) + assert math.isclose(result[1][1], 0.0, abs_tol=1e-9) + + +def test_rot_2d_pi() -> None: + """Testet die 2D-Rotation um pi Radiant (180 Grad).""" + # Arrange + angle = math.pi + + # Act + result = rot_2d(angle) + + # Assert + assert math.isclose(result[0][0], -1.0, abs_tol=1e-9) + assert math.isclose(result[0][1], 0.0, abs_tol=1e-9) + assert math.isclose(result[1][0], 0.0, abs_tol=1e-9) + assert math.isclose(result[1][1], -1.0, abs_tol=1e-9) + + +def test_rot_2d_negative_angle() -> None: + """Testet die 2D-Rotation mit einem negativen Winkel (-pi / 2).""" + # Arrange + angle = -math.pi / 2 + + # Act + result = rot_2d(angle) + + # Assert + assert math.isclose(result[0][0], 0.0, abs_tol=1e-9) + assert math.isclose(result[0][1], 1.0, abs_tol=1e-9) + assert math.isclose(result[1][0], -1.0, abs_tol=1e-9) + assert math.isclose(result[1][1], 0.0, abs_tol=1e-9) + + +def test_rot_3d_x_axis_zero() -> None: + """Testet die 3D-Rotation um die x-Achse um 0 Radiant.""" + # Arrange + angle = 0.0 + axis = "x" + + # Act + result = rot_3d(angle, axis) + + # Assert + expected = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + for row in range(3): + for col in range(3): + assert math.isclose(result[row][col], expected[row][col], abs_tol=1e-9) + + +def test_rot_3d_x_axis_pi_half() -> None: + """Testet die 3D-Rotation um die x-Achse um pi / 2 Radiant.""" + # Arrange + angle = math.pi / 2 + axis = "x" + + # Act + result = rot_3d(angle, axis) + + # Assert + expected = [ + [1.0, 0.0, 0.0], + [0.0, 0.0, -1.0], + [0.0, 1.0, 0.0], + ] + for row in range(3): + for col in range(3): + assert math.isclose(result[row][col], expected[row][col], abs_tol=1e-9) + + +def test_rot_3d_y_axis_zero() -> None: + """Testet die 3D-Rotation um die y-Achse um 0 Radiant.""" + # Arrange + angle = 0.0 + axis = "y" + + # Act + result = rot_3d(angle, axis) + + # Assert + expected = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + for row in range(3): + for col in range(3): + assert math.isclose(result[row][col], expected[row][col], abs_tol=1e-9) + + +def test_rot_3d_y_axis_pi_half() -> None: + """Testet die 3D-Rotation um die y-Achse um pi / 2 Radiant.""" + # Arrange + angle = math.pi / 2 + axis = "y" + + # Act + result = rot_3d(angle, axis) + + # Assert + expected = [ + [0.0, 0.0, 1.0], + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + ] + for row in range(3): + for col in range(3): + assert math.isclose(result[row][col], expected[row][col], abs_tol=1e-9) + + +def test_rot_3d_z_axis_zero() -> None: + """Testet die 3D-Rotation um die z-Achse um 0 Radiant.""" + # Arrange + angle = 0.0 + axis = "z" + + # Act + result = rot_3d(angle, axis) + + # Assert + expected = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + for row in range(3): + for col in range(3): + assert math.isclose(result[row][col], expected[row][col], abs_tol=1e-9) + + +def test_rot_3d_z_axis_pi_half() -> None: + """Testet die 3D-Rotation um die z-Achse um pi / 2 Radiant.""" + # Arrange + angle = math.pi / 2 + axis = "z" + + # Act + result = rot_3d(angle, axis) + + # Assert + expected = [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + for row in range(3): + for col in range(3): + assert math.isclose(result[row][col], expected[row][col], abs_tol=1e-9) + + +@pytest.mark.parametrize("invalid_axis", ["w", "123", "", "X", "Y", "Z"]) +def test_rot_3d_invalid_axis(invalid_axis: str) -> None: + """Testet, dass ungültige Achsen einen ValueError auslösen.""" + # Arrange + angle = 1.0 + + # Act & Assert + with pytest.raises(ValueError, match="Ungültige Achse"): + rot_3d(angle, invalid_axis)