""" Rechenhilfen für Matrizen. Dieses Modul stellt vier Funktionen bereit: * :func:`matmul` – multipliziert zwei (auch nicht-quadratische) Matrizen. * :func:`transpose` – transponiert eine Matrix. * :func:`rotate` – rotiert eine Matrix um 90 Grad im Uhrzeigersinn. * :func:`rot_2D` – erzeugt die 2x2-Rotationsmatrix für einen Winkel. Die Implementierung validiert alle Eingaben: * Alle Zeilen einer Matrix müssen gleich lang sein. * Bei :func:`matmul` muss die Spaltenzahl der ersten Matrix der Zeilenzahl der zweiten Matrix entsprechen – die klassische Regel der Matrixmultiplikation. * Alle Elemente müssen ``int`` oder ``float`` sein – andernfalls wird ein :class:`TypeError` ausgelöst. Nicht kompatible Matrizen führen zu einem :class:`ValueError`. Unter ``__main__`` befindet sich ein kleiner Testrahmen zum schnellen manuellen Ausprobieren. Für dieses Rechenmodul werden keine externen Abhängigkeiten benötigt. """ from __future__ import annotations from tabulate import tabulate import math __all__ = ["matmul", "rot_2D", "rotate", "transpose"] def _validate(matrix: list[list[float]], name: str) -> tuple[int, int]: """Prüfe eine Matrix und gib ihre Form ``(Zeilen, Spalten)`` zurück. Akzeptiert auch die leere Matrix ``[]`` (Form ``(0, 0)``). Alle Zeilen müssen gleich lang und alle Elemente müssen ``int`` oder ``float`` sein. Raises ------ ValueError Wenn die Zeilen ungleich lang sind. TypeError Wenn ein Element keine Zahl ist. """ if not matrix: return (0, 0) n_rows = len(matrix) n_cols = len(matrix[0]) for row in matrix: if len(row) != n_cols: raise ValueError( f"Alle Zeilen von {name} müssen gleich lang sein " f"(erwartet {n_cols}, erhalten {len(row)})" ) for x in row: if not isinstance(x, (int, float)): raise TypeError( f"Elemente von {name} müssen Zahlen sein, " f"aber {x!r} ist {type(x).__name__}" ) return (n_rows, n_cols) # ================= Matrixmultiplikation ================= def matmul( a: list[list[float]], b: list[list[float]] ) -> list[list[float]]: """Berechne das Produkt ``a @ b`` zweier Matrizen. Die Matrizen dürfen auch rechteckig sein. Beide Eingaben werden nicht verändert. Parameters ---------- a, b : list[list[float]] Zweidimensionale Listen mit Zahlen. Die Spaltenzahl von ``a`` muss der Zeilenzahl von ``b`` entsprechen. Die leere Matrix (``[]``) ist als Operand nicht erlaubt. Returns ------- list[list[float]] Das Matrixprodukt ``a @ b``. Raises ------ ValueError Wenn eine der Matrizen leer ist, Zeilen ungleich lang sind oder die Dimensionen nicht zusammenpassen. TypeError Wenn ein Element keine Zahl ist. """ rows_a, cols_a = _validate(a, "'a'") rows_b, cols_b = _validate(b, "'b'") if rows_a == 0 or rows_b == 0: raise ValueError( "Leere Matrizen sind als Operanden der Multiplikation " "nicht erlaubt" ) if cols_a != rows_b: raise ValueError( f"Spaltenzahl von 'a' ({cols_a}) und Zeilenzahl von 'b' " f"({rows_b}) müssen übereinstimmen" ) # Initialisiere die Ergebnismatrix (rows_a x cols_b) mit Nullen. result: list[list[float]] = [[0] * cols_b for _ in range(rows_a)] # Zeilenweise durchlaufen (i), dann das gemeinsame Element (k): # ikj-Reihenfolge ist cache-freundlicher als das klassische ijk. for i in range(rows_a): row_result = result[i] for k in range(cols_a): aik = a[i][k] if aik == 0: continue b_row = b[k] for j in range(cols_b): row_result[j] += aik * b_row[j] return result # ================= Transposition ================= def transpose(matrix: list[list[float]]) -> list[list[float]]: """Gib die Transponierte einer Matrix zurück. Parameters ---------- matrix : list[list[float]] Zweidimensionale Liste mit Zahlen. Die leere Matrix ``[]`` wird als ``[]`` zurückgegeben. Returns ------- list[list[float]] Die transponierte Matrix. Raises ------ ValueError Wenn die Zeilen ungleich lang sind. TypeError Wenn ein Element keine Zahl ist. """ n_rows, n_cols = _validate(matrix, "matrix") return [[matrix[i][j] for i in range(n_rows)] for j in range(n_cols)] # ================= Rotation um 90 Grad ================= def rotate(matrix: list[list[float]]) -> list[list[float]]: """Rotiere eine Matrix um 90 Grad im Uhrzeigersinn. Die erste Zeile wird zur letzten Spalte, die letzte Zeile zur ersten Spalte. Die Eingabe wird nicht verändert. Parameters ---------- matrix : list[list[float]] Zweidimensionale Liste mit Zahlen. Die leere Matrix ``[]`` wird als ``[]`` zurückgegeben. Returns ------- list[list[float]] Die um 90 Grad im Uhrzeigersinn rotierte Matrix. Raises ------ ValueError Wenn die Zeilen ungleich lang sind. TypeError Wenn ein Element keine Zahl ist. """ return [row[::-1] for row in transpose(matrix)] # ================= 2D-Rotationsmatrix ================= def rot_2D(theta: float) -> list[list[float]]: """Erzeuge die Rotationsmatrix für eine Drehung um ``theta``. Die Matrix dreht einen Punkt in der Ebene um den Winkel ``theta`` um den Ursprung: .. math:: \\begin{pmatrix} \\cos\\theta & -\\sin\\theta \\\\ \\sin\\theta & \\cos\\theta \\end{pmatrix} Parameters ---------- theta : float Drehwinkel im Bogenmaß (Radiant). Returns ------- list[list[float]] Die 2x2-Rotationsmatrix. """ cos_t = math.cos(theta) sin_t = math.sin(theta) return [[cos_t, -sin_t], [sin_t, cos_t]] # ================= Testrahmen ================= if __name__ == "__main__": # matmul mit rechteckigen Matrizen matrix_a = [ [3, 4, -1, 4], [-2, 2, 5, 1], ] matrix_b = [ [1, 3, -2], [2, 5, 1], [-1, 4, -4], [2, 3, 6], ] matrix_c = matmul(matrix_a, matrix_b) print("Ergebnis C = A * B:") for row in matrix_c: print(row) # rot_2D: Drehung um 90 Grad gegen den Uhrzeigersinn rotation = rot_2D(math.pi / 2) point = [[1.0], [0.0]] print("rot_2D-Ergebnis:", matmul(rotation, point)) print(tabulate(rot_2D(1.0))) # Git - Test