235 lines
6.5 KiB
Python
235 lines
6.5 KiB
Python
"""Rechen-Hilfen 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
|
||
|
||
import math
|
||
|
||
__all__ = ["matmul", "transpose", "rotate", "rot_2D"]
|
||
|
||
|
||
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))
|
||
|