diff --git a/AGENTS.md b/AGENTS.md index df42e17..6faf521 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,4 +2,6 @@ - Code nach PEP 8, Zeilenlänge maximal 88 Zeichen. - Jede Funktion und Methode hat einen Docstring und Type Hints. -- Vor jedem Commit: `ruff check` und `ruff format` \ No newline at end of file +- Vor jedem Commit: `ruff check` und `ruff format` + +## Projektstruktur - Quellcode liegt unter "src/matrixmania/", Tests unter "test/test_matrixmania/" - Testpakete, Testdateien und Testfunktionen beginnen mit "test_" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3a776b1..48c5fd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,11 @@ [project] name = "matrixmania" version = "0.1.0" -dependencies = ["tabulate", "ruff"] \ No newline at end of file +dependencies = ["tabulate", "ruff"] + +[project.optional-dependencies] +dev = ["pytest", "ruff", "pytest-cov"] + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["test"] diff --git a/src/matrixmania/__init__.py b/src/matrixmania/__init__.py new file mode 100644 index 0000000..c812c86 --- /dev/null +++ b/src/matrixmania/__init__.py @@ -0,0 +1 @@ +from .compute import matmul, transpose, rot_2D, rot_3D \ No newline at end of file diff --git a/compute.py b/src/matrixmania/compute.py similarity index 71% rename from compute.py rename to src/matrixmania/compute.py index 854b914..733c45c 100644 --- a/compute.py +++ b/src/matrixmania/compute.py @@ -1,3 +1,12 @@ +#Katzenaufgabe: 1. funktionierender Stand war committed. +#2. in compute.py und pyproject.toml absichtlich Änderungen/Quatsch gemacht. +#3. Diese Änderungen nicht committed. +#4. ↩ Git → Uncommitted Changes → Rollback ausgeführt. +#5. Danach waren die Katzen-Änderungen wieder verschwunden und die Dateien wieder wie vorher. + +#Dann: Katze erledigt. + + # Matrixfunktionen für das Projekt MatrixMania #Was die Aufgabe war: @@ -20,6 +29,7 @@ """Matrix-Operationen ohne externe Bibliotheken (reine Python-Listen).""" import math +from math import cos, sin from tabulate import tabulate @@ -117,6 +127,82 @@ def rot_2D(theta: float) -> Matrix: return [[cosine, -sine], [sine, cosine]] +def rot_3D(angle: float, axis: str) -> list[list[float]]: + """Erzeugt eine Rotationsmatrix für eine Drehung im 3D-Raum. + + Args: + angle: Drehwinkel im Bogenmaß. + axis: Drehachse "x", "y" oder "z". + + Returns: + Die zur Achse passende 3x3-Rotationsmatrix. + + Raises: + ValueError: Wenn axis weder "x", "y" noch "z" ist. + + Formeln mit c = cos(angle) und s = sin(angle): + + Rotation um die x-Achse: + [1, 0, 0] + [0, c, -s] + [0, s, c] + + Rotation um die y-Achse: + [ c, 0, s] + [ 0, 1, 0] + [-s, 0, c] + + Rotation um die z-Achse: + [c, -s, 0] + [s, c, 0] + [0, 0, 1] + """ + c = cos(angle) + s = sin(angle) + + if axis == "x": + return [ + [1.0, 0.0, 0.0], + [0.0, c, -s], + [0.0, s, c], + ] + + if axis == "y": + return [ + [c, 0.0, s], + [0.0, 1.0, 0.0], + [-s, 0.0, c], + ] + + if axis == "z": + return [ + [c, -s, 0.0], + [s, c, 0.0], + [0.0, 0.0, 1.0], + ] + + raise ValueError("axis muss 'x', 'y' oder 'z' sein.") + + +def project_ortho( + point: tuple[float, float, float], + scale: float = 1, +) -> tuple[float, float]: + """Projiziert einen 3D-Punkt auf die xy-Ebene und skaliert ihn.""" + + # Die ersten beiden Koordinaten des Punktes auslesen. + # Python beginnt beim Zählen mit 0. + x = point[0] + y = point[1] + + # Die dritte Koordinate point[2] ist z. + # Für diese Projektion brauchen wir sie nicht. + + # x und y skalieren und als Tupel zurückgeben. + return (x * scale, y * scale) + + + if __name__ == "__main__": #führe den testcode nur aus wenn compute.py direkt gestartet wird matrix_a = [ [3, 4, -1, 4], diff --git a/test/test_matrixmania/__init__.py b/test/test_matrixmania/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_matrixmania/test_rot.py b/test/test_matrixmania/test_rot.py new file mode 100644 index 0000000..4a07692 --- /dev/null +++ b/test/test_matrixmania/test_rot.py @@ -0,0 +1,96 @@ +from math import pi + +import pytest + +from matrixmania import rot_2D, rot_3D + + +def test_rot_2D_90_degrees(): + # Arrange + angle = pi / 2 + expected = [ + [0.0, -1.0], + [1.0, 0.0], + ] + + # Act + result = rot_2D(angle) + + # Assert + assert len(result) == len(expected) + for actual_row, expected_row in zip(result, expected): + assert actual_row == pytest.approx(expected_row) + + +def test_rot_2D_invalid_angle(): + # Arrange + angle = "ungueltig" + + # Act & Assert: + # Aufruf ausführen und prüfen, dass ein TypeError entsteht. + with pytest.raises(TypeError): + rot_2D(angle) + + +@pytest.mark.parametrize( + "axis, expected", + [ + ( + "x", + [ + [1.0, 0.0, 0.0], + [0.0, 0.0, -1.0], + [0.0, 1.0, 0.0], + ], + ), + ( + "y", + [ + [0.0, 0.0, 1.0], + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + ], + ), + ( + "z", + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ], + ), + ], +) +def test_rot_3D_90_degrees(axis, expected): + # Arrange + angle = pi / 2 + + # Act + result = rot_3D(angle, axis) + + # Assert + assert len(result) == len(expected) + for actual_row, expected_row in zip(result, expected): + assert actual_row == pytest.approx(expected_row) + + +@pytest.mark.parametrize("axis", ["a", "", "X"]) +def test_rot_3D_invalid_axis(axis): + # Arrange + angle = pi / 2 + + # Act & Assert: + # Aufruf ausführen und den erwarteten ValueError prüfen. + with pytest.raises(ValueError): + rot_3D(angle, axis) + + +def test_rot_3D_invalid_angle(): + # Arrange + angle = "ungueltig" + axis = "x" + + # Act & Assert: + # Aufruf ausführen und den erwarteten TypeError prüfen. + with pytest.raises(TypeError): + rot_3D(angle, axis) \ No newline at end of file