\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 79a2eba..c08a655 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,4 +1,8 @@
# Regeln für dieses Projekt
- 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..36102ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,4 +1,8 @@
[project]
name = "matrixmania"
version = "0.1.0"
-dependencies = ["tabulate", "ruff"]
\ No newline at end of file
+dependencies = ["tabulate", "ruff"]
+[tool.pytest.ini_options]
+pythonpath = "src"
+[project.optional-dependencies]
+dev = ["pytest", "ruff", "pytest-cov"]
\ No newline at end of file
diff --git a/src/matrixmania/__init__.py b/src/matrixmania/__init__.py
new file mode 100644
index 0000000..a68d144
--- /dev/null
+++ b/src/matrixmania/__init__.py
@@ -0,0 +1,3 @@
+from .compute import matmul, rot_2d, rot_3d, transpose
+
+__all__ = ["matmul", "rot_2d", "rot_3d", "transpose"]
diff --git a/compute.py b/src/matrixmania/compute.py
similarity index 55%
rename from compute.py
rename to src/matrixmania/compute.py
index 10d2a17..55324c6 100644
--- a/compute.py
+++ b/src/matrixmania/compute.py
@@ -1,246 +1,191 @@
-"""Modul zur Matrizenmultiplikation."""
-
-import math
-import sys
-from numbers import Number
-from tabulate import tabulate
-
-def _validate_matrix(matrix: object, name: str) -> list[list[Number]]:
- """Prüft, ob es sich um eine gültige Matrix handelt.
-
- Args:
- matrix (object): Zu prüfende Matrix.
- name (str): Name der Matrix für die Fehlermeldung.
-
- Returns:
- list[list[Number]]: Validierte Matrix.
-
- Raises:
- TypeError: Wenn die Matrix keine Liste ist oder nicht-numerische
- Elemente enthält.
- ValueError: Wenn die Matrix leer ist oder unregelmäßige Zeilenlängen
- aufweist.
- """
- if not isinstance(matrix, list):
- raise TypeError(f"'{name}' muss eine Liste von Listen sein, nicht {type(matrix).__name__}.")
-
- if len(matrix) == 0:
- raise ValueError(f"'{name}' darf nicht leer sein.")
-
- for index, row in enumerate(matrix):
- if not isinstance(row, list):
- raise TypeError(f"Zeile {index} von '{name}' muss eine Liste sein, nicht {type(row).__name__}.")
-
- if len(row) == 0:
- raise ValueError(f"Zeile {index} von '{name}' darf nicht leer sein.")
-
- if len(row) != len(matrix[0]):
- raise ValueError(
- f"Zeile {index} von '{name}' hat {len(row)} Spalten, erwartet werden {len(matrix[0])}."
- )
-
- for column, value in enumerate(row):
- if isinstance(value, bool) or not isinstance(value, Number):
- raise TypeError(
- f"Element [{index}][{column}] von '{name}' muss eine Zahl sein, "
- f"nicht {type(value).__name__}."
- )
-
- return matrix
-
-
-def matmul(matrix_a: list[list[Number]], matrix_b: list[list[Number]]) -> list[list[Number]]:
- """Multipliziert zwei Matrizen.
-
- Die Anzahl der Spalten von ``matrix_a`` muss mit der Anzahl der Zeilen von
- ``matrix_b`` übereinstimmen.
-
- Args:
- matrix_a (list[list[Number]]): Erste Matrix der Form m x n.
- matrix_b (list[list[Number]]): Zweite Matrix der Form n x p.
-
- Returns:
- list[list[Number]]: Ergebnis der Multiplikation der Form m x p.
-
- Raises:
- TypeError: Wenn eine der Eingaben keine gültige Matrix aus Zahlen ist.
- ValueError: Wenn die Matrizen leer sind oder die Dimensionen nicht
- zueinander passen.
- """
- matrix_a = _validate_matrix(matrix_a, "matrix_a")
- matrix_b = _validate_matrix(matrix_b, "matrix_b")
-
- rows_a = len(matrix_a)
- columns_a = len(matrix_a[0])
- rows_b = len(matrix_b)
- columns_b = len(matrix_b[0])
-
- if columns_a != rows_b:
- raise ValueError(
- f"Dimensionen passen nicht: matrix_a hat {columns_a} Spalten, "
- f"matrix_b hat {rows_b} Zeilen."
- )
-
- result: list[list[Number]] = [[0 for _ in range(columns_b)] for _ in range(rows_a)]
-
- for i in range(rows_a):
- for j in range(columns_b):
- total = 0
- for k in range(columns_a):
- total += matrix_a[i][k] * matrix_b[k][j]
- result[i][j] = total
-
- return result
-
-
-def transpose(matrix: list[list[Number]]) -> list[list[Number]]:
- """Transponiert eine Matrix.
-
- Vertauscht Zeilen und Spalten der Matrix. Aus einer Matrix der Form m x n
- wird eine Matrix der Form n x m.
-
- Args:
- matrix (list[list[Number]]): Zu transponierende Matrix.
-
- Returns:
- list[list[Number]]: Transponierte Matrix der Form n x m.
-
- Raises:
- TypeError: Wenn die Eingabe keine gültige Matrix aus Zahlen ist.
- ValueError: Wenn die Matrix leer ist oder unregelmäßige Zeilenlängen
- aufweist.
- """
- matrix = _validate_matrix(matrix, "matrix")
-
- rows = len(matrix)
- columns = len(matrix[0])
-
- return [[matrix[i][j] for i in range(rows)] for j in range(columns)]
-
-
-def rotate(angle: float) -> list[list[float]]:
- """Ergänzter Kommentar"""
- """Erstellt die Rotationsmatrix für einen Winkel.
-
- Args:
- angle (float): Drehwinkel im Bogenmaß.
-
- Returns:
- list[list[float]]: 2x2-Rotationsmatrix, die auf einen Vektor
- angewendet werden kann.
-
- Raises:
- TypeError: Wenn ``angle`` keine Zahl ist.
- """
-
- if isinstance(angle, bool) or not isinstance(angle, Number):
- raise TypeError(f"'angle' muss eine Zahl sein, nicht {type(angle).__name__}.")
-
- cosine = math.cos(angle)
- sine = math.sin(angle)
-
- return [[cosine, -sine], [sine, cosine]]
-
-
-def _parse_number(token: str) -> float:
- """Wandelt einen eingegebenen Text in eine Zahl um.
-
- Args:
- token (str): Einzelner Eingabewert.
-
- Returns:
- float: Zahlwert des Eingabetokens.
-
- Raises:
- ValueError: Wenn der Text keine gültige Zahl ist.
- """
- try:
- return float(token)
- except ValueError as error:
- raise ValueError(f"'{token}' ist keine gültige Zahl.") from error
-
-
-def _read_matrix(name: str) -> list[list[float]]:
- """Liest eine Matrix zeilenweise aus der Konsole ein.
-
- Der Benutzer gibt zuerst die Anzahl der Zeilen an. Anschließend wird jede
- Zeile als Leerzeichen-getrennte Liste von Zahlen eingegeben. Bei ungültiger
- Eingabe wird die jeweilige Eingabe erneut abgefragt.
-
- Args:
- name (str): Name der Matrix für die Ausgabe.
-
- Returns:
- list[list[float]]: Eingelesene Matrix.
- """
- print(f"\nEingabe der Matrix {name}:")
-
- while True:
- raw = input("Anzahl der Zeilen: ").strip()
- try:
- rows = int(raw)
- except ValueError:
- print(f"Fehler: '{raw}' ist keine ganze Zahl. Bitte erneut eingeben.")
- continue
- if rows <= 0:
- print("Fehler: Die Anzahl der Zeilen muss größer als 0 sein.")
- continue
- break
-
- matrix: list[list[float]] = []
- expected_columns: int | None = None
-
- for index in range(rows):
- while True:
- raw_row = input(f"Zeile {index} (Zahlen durch Leerzeichen getrennt): ").strip()
- tokens = raw_row.split()
-
- if not tokens:
- print("Fehler: Die Zeile darf nicht leer sein. Bitte erneut eingeben.")
- continue
-
- try:
- row = [_parse_number(token) for token in tokens]
- except ValueError as error:
- print(f"Fehler: {error} Bitte erneut eingeben.")
- continue
-
- if expected_columns is None:
- expected_columns = len(row)
- elif len(row) != expected_columns:
- print(
- f"Fehler: Die Zeile hat {len(row)} Spalten, "
- f"erwartet werden {expected_columns}. Bitte erneut eingeben."
- )
- continue
-
- matrix.append(row)
- break
-
- return matrix
-
-
-def _print_matrix(matrix: list[list[Number]]) -> None:
- """Gibt eine Matrix formatiert auf der Konsole aus.
-
- Args:
- matrix (list[list[Number]]): Auszugebende Matrix.
- """
- for row in matrix:
- print(" [" + ", ".join(str(value) for value in row) + "]")
-
-
-def main() -> int:
- """Startet das Programm und liest zwei Matrizen zur Multiplikation ein.
-
- Returns:
- int: Exit-Code des Programms (0 bei Erfolg, 1 bei Fehler).
- """
- #print(rotate(45))
- print(tabulate(rotate(1.0)))
-
-
-
-
-if __name__ == "__main__":
- sys.exit(main())
+"""Modul zur Matrizenmultiplikation."""
+
+import math
+from numbers import Number
+
+
+def _validate_matrix(matrix: object, name: str) -> list[list[Number]]:
+ """Prüft, ob es sich um eine gültige Matrix handelt.
+
+ Args:
+ matrix (object): Zu prüfende Matrix.
+ name (str): Name der Matrix für die Fehlermeldung.
+
+ Returns:
+ list[list[Number]]: Validierte Matrix.
+
+ Raises:
+ TypeError: Wenn die Matrix keine Liste ist oder nicht-numerische
+ Elemente enthält.
+ ValueError: Wenn die Matrix leer ist oder unregelmäßige Zeilenlängen
+ aufweist.
+ """
+ if not isinstance(matrix, list):
+ raise TypeError(
+ f"'{name}' muss eine Liste von Listen sein, nicht {type(matrix).__name__}."
+ )
+
+ if len(matrix) == 0:
+ raise ValueError(f"'{name}' darf nicht leer sein.")
+
+ for index, row in enumerate(matrix):
+ if not isinstance(row, list):
+ raise TypeError(
+ f"Zeile {index} von '{name}' muss eine Liste sein, nicht {type(row).__name__}."
+ )
+
+ if len(row) == 0:
+ raise ValueError(f"Zeile {index} von '{name}' darf nicht leer sein.")
+
+ if len(row) != len(matrix[0]):
+ raise ValueError(
+ f"Zeile {index} von '{name}' hat {len(row)} Spalten, erwartet werden {len(matrix[0])}."
+ )
+
+ for column, value in enumerate(row):
+ if isinstance(value, bool) or not isinstance(value, Number):
+ raise TypeError(
+ f"Element [{index}][{column}] von '{name}' muss eine Zahl sein, "
+ f"nicht {type(value).__name__}."
+ )
+
+ return matrix
+
+
+def matmul(
+ matrix_a: list[list[Number]], matrix_b: list[list[Number]]
+) -> list[list[Number]]:
+ """Multipliziert zwei Matrizen.
+
+ Die Anzahl der Spalten von ``matrix_a`` muss mit der Anzahl der Zeilen von
+ ``matrix_b`` übereinstimmen.
+
+ Args:
+ matrix_a (list[list[Number]]): Erste Matrix der Form m x n.
+ matrix_b (list[list[Number]]): Zweite Matrix der Form n x p.
+
+ Returns:
+ list[list[Number]]: Ergebnis der Multiplikation der Form m x p.
+
+ Raises:
+ TypeError: Wenn eine der Eingaben keine gültige Matrix aus Zahlen ist.
+ ValueError: Wenn die Matrizen leer sind oder die Dimensionen nicht
+ zueinander passen.
+ """
+ matrix_a = _validate_matrix(matrix_a, "matrix_a")
+ matrix_b = _validate_matrix(matrix_b, "matrix_b")
+
+ rows_a = len(matrix_a)
+ columns_a = len(matrix_a[0])
+ rows_b = len(matrix_b)
+ columns_b = len(matrix_b[0])
+
+ if columns_a != rows_b:
+ raise ValueError(
+ f"Dimensionen passen nicht: matrix_a hat {columns_a} Spalten, "
+ f"matrix_b hat {rows_b} Zeilen."
+ )
+
+ result: list[list[Number]] = [[0 for _ in range(columns_b)] for _ in range(rows_a)]
+
+ for i in range(rows_a):
+ for j in range(columns_b):
+ total = 0
+ for k in range(columns_a):
+ total += matrix_a[i][k] * matrix_b[k][j]
+ result[i][j] = total
+
+ return result
+
+
+def transpose(matrix: list[list[Number]]) -> list[list[Number]]:
+ """Transponiert eine Matrix.
+
+ Vertauscht Zeilen und Spalten der Matrix. Aus einer Matrix der Form m x n
+ wird eine Matrix der Form n x m.
+
+ Args:
+ matrix (list[list[Number]]): Zu transponierende Matrix.
+
+ Returns:
+ list[list[Number]]: Transponierte Matrix der Form n x m.
+
+ Raises:
+ TypeError: Wenn die Eingabe keine gültige Matrix aus Zahlen ist.
+ ValueError: Wenn die Matrix leer ist oder unregelmäßige Zeilenlängen
+ aufweist.
+ """
+ matrix = _validate_matrix(matrix, "matrix")
+
+ rows = len(matrix)
+ columns = len(matrix[0])
+
+ return [[matrix[i][j] for i in range(rows)] for j in range(columns)]
+
+
+def rot_2d(angle: float) -> list[list[float]]:
+ """Erstellt die Rotationsmatrix für einen Winkel.
+
+ Args:
+ angle (float): Drehwinkel im Bogenmaß.
+
+ Returns:
+ list[list[float]]: 2x2-Rotationsmatrix, die auf einen Vektor
+ angewendet werden kann.
+
+ Raises:
+ TypeError: Wenn ``angle`` keine Zahl ist.
+ """
+
+ if isinstance(angle, bool) or not isinstance(angle, Number):
+ raise TypeError(f"'angle' muss eine Zahl sein, nicht {type(angle).__name__}.")
+
+ cosine = math.cos(angle)
+ sine = math.sin(angle)
+
+ return [[cosine, -sine], [sine, cosine]]
+
+
+def rot_3d(angle: float, axis: str) -> list[list[float]]:
+ """Erstellt die 3D-Rotationsmatrix für einen Winkel um eine Achse.
+
+ Die Rotation erfolgt im Uhrzeigersinn (Rechtssystem) um die angegebene
+ Achse. Die zurückgegebene Matrix ist 3x3 groß und kann direkt auf einen
+ 3D-Vektor angewendet werden.
+
+ Args:
+ angle (float): Drehwinkel im Bogenmaß.
+ axis (str): Rotationsachse, eine von ``"x"``, ``"y"`` oder ``"z"``.
+ Groß- und Kleinschreibung spielt keine Rolle.
+
+ Returns:
+ list[list[float]]: 3x3-Rotationsmatrix, die auf einen 3D-Vektor
+ angewendet werden kann.
+
+ Raises:
+ TypeError: Wenn ``angle`` keine Zahl oder ``axis`` kein String ist.
+ ValueError: Wenn ``axis`` keine der drei Hauptachsen ist.
+ """
+ if isinstance(angle, bool) or not isinstance(angle, Number):
+ raise TypeError(f"'angle' muss eine Zahl sein, nicht {type(angle).__name__}.")
+
+ if not isinstance(axis, str):
+ raise TypeError(f"'axis' muss ein String sein, nicht {type(axis).__name__}.")
+
+ normalized_axis = axis.strip().lower()
+
+ if normalized_axis not in ("x", "y", "z"):
+ raise ValueError(
+ f"'axis' muss eine der Achsen 'x', 'y' oder 'z' sein, nicht '{axis}'."
+ )
+
+ cosine = math.cos(angle)
+ sine = math.sin(angle)
+
+ if normalized_axis == "x":
+ return [[1.0, 0.0, 0.0], [0.0, cosine, -sine], [0.0, sine, cosine]]
+
+ if normalized_axis == "y":
+ return [[cosine, 0.0, sine], [0.0, 1.0, 0.0], [-sine, 0.0, cosine]]
+
+ return [[cosine, -sine, 0.0], [sine, cosine, 0.0], [0.0, 0.0, 1.0]]
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..843f2ea
--- /dev/null
+++ b/test/test_matrixmania/test_rot.py
@@ -0,0 +1,78 @@
+"""Tests für die Rotationsfunktionen."""
+
+import math
+
+import pytest
+
+from matrixmania.compute import rot_2d, rot_3d
+
+
+def _flatten(matrix: list[list[float]]) -> list[float]:
+ """Flacht eine Matrix zu einer flachen Liste ab.
+
+ Args:
+ matrix (list[list[float]]): Zu flachende Matrix.
+
+ Returns:
+ list[float]: Alle Matrixelemente in Zeilen-Reihenfolge.
+ """
+ return [value for row in matrix for value in row]
+
+
+@pytest.mark.parametrize(
+ ("angle", "expected"),
+ [
+ (0.0, [1.0, 0.0, 0.0, 1.0]),
+ (math.pi / 2, [0.0, -1.0, 1.0, 0.0]),
+ (math.pi, [-1.0, 0.0, 0.0, -1.0]),
+ (2 * math.pi, [1.0, 0.0, 0.0, 1.0]),
+ ],
+)
+def test_rot_2d(angle: float, expected: list[float]) -> None:
+ """Prüft die 2D-Rotationsmatrix für einen Winkel.
+
+ Args:
+ angle (float): Drehwinkel im Bogenmaß.
+ expected (list[float]): Erwartete Matrixwerte in Zeilen-Reihenfolge.
+
+ Raises:
+ AssertionError: Wenn die Rotationsmatrix nicht erwartet wird.
+ """
+ # Arrange: Winkel und Erwartungswerte stammen aus der Parametrisierung.
+
+ # Act
+ result = _flatten(rot_2d(angle))
+
+ # Assert
+ assert result == pytest.approx(expected)
+
+
+@pytest.mark.parametrize(
+ ("angle", "axis", "expected"),
+ [
+ (0.0, "x", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
+ (0.0, "y", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
+ (0.0, "z", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
+ (math.pi / 2, "x", [1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0]),
+ (math.pi / 2, "y", [0.0, 0.0, 1.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0]),
+ (math.pi / 2, "z", [0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
+ ],
+)
+def test_rot_3d(angle: float, axis: str, expected: list[float]) -> None:
+ """Prüft die 3D-Rotationsmatrix für einen Winkel und eine Achse.
+
+ Args:
+ angle (float): Drehwinkel im Bogenmaß.
+ axis (str): Rotationsachse ("x", "y" oder "z").
+ expected (list[float]): Erwartete Matrixwerte in Zeilen-Reihenfolge.
+
+ Raises:
+ AssertionError: Wenn die Rotationsmatrix nicht erwartet wird.
+ """
+ # Arrange: Winkel, Achse und Erwartungswerte stammen aus der Parametrisierung.
+
+ # Act
+ result = _flatten(rot_3d(angle, axis))
+
+ # Assert
+ assert result == pytest.approx(expected)