install_dependencies.py, test_projection.py, test_rot.py hinzugefügt, compute.py und pyproject.toml modified

This commit is contained in:
englingvi103466 committed 2026-10-22 13:09:17 +02:00
1 parent bdbf024154
commit 806434d813
5 files changed
+254 -79

No files matched your search

+51 -79
View File
@@ -1,8 +1,7 @@
"""Compute operations for matrices. """Compute operations for matrices.
This module provides public functions for matrix multiplication, transposition, This module provides public functions for matrix multiplication, transposition,
matrix rotation, and generating 2D rotation matrices. The implementation is matrix rotation, and generating 2D and 3D rotation matrices as well as projections.
lightweight, well typed and contains detailed Google‑style documentation.
""" """
from __future__ import annotations from __future__ import annotations
@@ -12,7 +11,7 @@ from typing import List
def matmul( def matmul(
matrix_a: List[List[float]], matrix_b: List[List[float]] matrix_a: List[List[float]], matrix_b: List[List[float]]
) -> List[List[float]]: ) -> List[List[float]]:
"""Multiply two 2D matrices. """Multiply two 2D matrices.
@@ -21,33 +20,25 @@ def matmul(
rows in ``matrix_b``. rows in ``matrix_b``.
Args: Args:
matrix_a: A matrix of shape (m, n) represented as a list of m rows, matrix_a: A matrix of shape (m, n) represented as a list of m rows.
each a list of n floats. matrix_b: A matrix of shape (n, p) represented as a list of n rows.
matrix_b: A matrix of shape (n, p) represented as a list of n rows,
each a list of p floats.
Returns: Returns:
A new matrix of shape (m, p) containing the product A new matrix of shape (m, p) containing the product ``matrix_a @ matrix_b``.
``matrix_a @ matrix_b``.
Raises: Raises:
ValueError: If either matrix is empty or if the column count of ValueError: If either matrix is empty or dimensions do not match.
``matrix_a`` does not match the row count of ``matrix_b``.
""" """
# SCHRITT 1: Validierung – Prüfen, ob die Matrizen überhaupt Daten enthalten
if not matrix_a or not matrix_b: if not matrix_a or not matrix_b:
raise ValueError("Input matrices must not be empty.") raise ValueError("Input matrices must not be empty.")
n_cols_a = len(matrix_a[0]) n_cols_a = len(matrix_a[0])
# SCHRITT 2: Konsistenzprüfung für Matrix A
if any(len(row) != n_cols_a for row in matrix_a): if any(len(row) != n_cols_a for row in matrix_a):
raise ValueError("All rows of matrix_a must have the same number of columns.") raise ValueError("All rows of matrix_a must have the same number of columns.")
n_rows_b = len(matrix_b) n_rows_b = len(matrix_b)
# SCHRITT 3: Mathematische Regel prüfen: Spalten von A müssen gleich Zeilen von B sein
if n_cols_a != n_rows_b: if n_cols_a != n_rows_b:
raise ValueError( raise ValueError(
f"Matrix dimension mismatch: matrix_a columns {n_cols_a} != matrix_b rows {n_rows_b}" f"Matrix dimension mismatch: matrix_a columns {n_cols_a} != matrix_b rows {n_rows_b}"
@@ -55,15 +46,13 @@ def matmul(
n_cols_b = len(matrix_b[0]) n_cols_b = len(matrix_b[0])
# SCHRITT 4: Konsistenzprüfung für Matrix B
if any(len(row) != n_cols_b for row in matrix_b): if any(len(row) != n_cols_b for row in matrix_b):
raise ValueError("All rows of matrix_b must have the same number of columns.") raise ValueError("All rows of matrix_b must have the same number of columns.")
# SCHRITT 5: Eigentliche Matrixmultiplikation (Skalarprodukt der Zeilen und Spalten)
result: List[List[float]] = [] result: List[List[float]] = []
for i in range(len(matrix_a)): # Gehe jede Zeile von Matrix A durch for i in range(len(matrix_a)):
row: List[float] = [] row: List[float] = []
for j in range(n_cols_b): # Gehe jede Spalte von Matrix B durch for j in range(n_cols_b):
elem = sum(matrix_a[i][k] * matrix_b[k][j] for k in range(n_cols_a)) elem = sum(matrix_a[i][k] * matrix_b[k][j] for k in range(n_cols_a))
row.append(elem) row.append(elem)
result.append(row) result.append(row)
@@ -72,18 +61,7 @@ def matmul(
def transpose(matrix: List[List[float]]) -> List[List[float]]: def transpose(matrix: List[List[float]]) -> List[List[float]]:
"""Return the transpose of a 2‑D matrix. """Return the transpose of a 2D matrix."""
Args:
matrix: A non‑empty matrix represented as a list of rows.
Returns:
A new matrix where rows become columns.
Raises:
ValueError: If the input matrix is empty or ragged.
"""
if not matrix: if not matrix:
raise ValueError("Cannot transpose an empty matrix.") raise ValueError("Cannot transpose an empty matrix.")
@@ -97,27 +75,11 @@ def transpose(matrix: List[List[float]]) -> List[List[float]]:
def rotate_clockwise(matrix: List[List[float]]) -> List[List[float]]: def rotate_clockwise(matrix: List[List[float]]) -> List[List[float]]:
"""Rotate a square matrix 90 degrees clockwise. """Rotate a square matrix 90 degrees clockwise."""
The function works for any rectangular matrix but will produce a matrix
of dimensions (cols, rows).
Args:
matrix: A non‑empty matrix.
Returns:
The rotated matrix.
Raises:
ValueError: If the matrix is empty.
"""
if not matrix: if not matrix:
raise ValueError("Cannot rotate an empty matrix.") raise ValueError("Cannot rotate an empty matrix.")
n_rows = len(matrix)
n_cols = len(matrix[0]) n_cols = len(matrix[0])
if any(len(row) != n_cols for row in matrix): if any(len(row) != n_cols for row in matrix):
raise ValueError("All rows of the matrix must have the same number of columns.") raise ValueError("All rows of the matrix must have the same number of columns.")
@@ -126,42 +88,52 @@ def rotate_clockwise(matrix: List[List[float]]) -> List[List[float]]:
def rot_2D(theta: float) -> List[List[float]]: def rot_2D(theta: float) -> List[List[float]]:
"""Return the 2D rotation matrix for a given angle. """Return the 2D rotation matrix for a given angle in degrees."""
rad = math.radians(theta)
Calculates the 2x2 rotation matrix used in computer graphics to rotate a point c = math.cos(rad)
counterclockwise around the origin by angle theta (in radians). s = math.sin(rad)
Args:
theta: The rotation angle in radians.
Returns:
A 2x2 rotation matrix represented as a list of lists of floats:
[[cos(theta), -sin(theta)],
[sin(theta), cos(theta)]]
"""
c = math.cos(theta)
s = math.sin(theta)
return [[c, -s], [s, c]] return [[c, -s], [s, c]]
def rot_3D(angle: float, axis: str) -> list[list[float]]:
"""Erzeugt eine 3D-Rotationsmatrix um die angegebene Achse ('x', 'y' oder 'z')."""
rad = math.radians(angle)
c, s = math.cos(rad), math.sin(rad)
axis = axis.lower()
if axis == 'x':
return [
[1.0, 0.0, 0.0],
[0.0, c, -s],
[0.0, s, c]
]
elif axis == 'y':
return [
[c, 0.0, s],
[0.0, 1.0, 0.0],
[-s, 0.0, c]
]
elif axis == 'z':
return [
[c, -s, 0.0],
[s, c, 0.0],
[0.0, 0.0, 1.0]
]
else:
raise ValueError("Achse muss 'x', 'y' oder 'z' sein.")
def project_ortho(point: tuple[float, float, float], scale: float = 1.0) -> tuple[float, float]:
"""Projiziert einen 3D-Punkt orthogonal auf eine 2D-Ebene und skaliert ihn."""
x, y, z = point
return (x * scale, y * scale)
if __name__ == "__main__": if __name__ == "__main__":
# Testdaten für die Demonstration
a = [[1, 2, 3], [4, 5, 6]] a = [[1, 2, 3], [4, 5, 6]]
b = [[7, 8], [9, 10], [11, 12]] b = [[7, 8], [9, 10], [11, 12]]
print("matmul(a, b):") print("matmul(a, b):", matmul(a, b))
print(matmul(a, b)) print("transpose(a):", transpose(a))
print("transpose(a):") # Teste deine Tests mit: python -m pytest
print(transpose(a))
c = [[1, 2], [3, 4], [5, 6]]
print("rotate_clockwise(c):")
print(rotate_clockwise(c))
# Demonstration von rot_2D (z. B. 90 Grad = pi / 2 Radiant)
angle = math.pi / 2
print(f"rot_2D (pi / 2 radians / 90 degrees):")
rotation_matrix = rot_2D(angle)
for row in rotation_matrix:
print([round(val, 4) for val in row])
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Installiert die Abhängigkeiten aus pyproject.toml in die aktuelle Python-Umgebung.
Ersatz für "Install requirements" aus PyCharm, wenn das Projekt eine pyproject.toml
statt einer requirements.txt hat. Kein Build, keine besondere Projektstruktur nötig,
nur Standardbibliothek (Python >= 3.11).
Benutzung:
In PyCharm: Rechtsklick auf diese Datei -> Run 'install_dependencies'
Im Terminal (venv aktiv): python install_dependencies.py
Ohne Parameter wird ALLES installiert:
[project].dependencies + alle [project.optional-dependencies] + alle [dependency-groups]
Abweichendes Verhalten:
python install_dependencies.py --extra dev nur Basis + Extra "dev"
python install_dependencies.py --group test nur Basis + Gruppe "test"
python install_dependencies.py --no-main --group dev
python install_dependencies.py --list nur anzeigen, nichts installieren
python install_dependencies.py --write requirements.txt schreiben statt installieren
python install_dependencies.py --write -o reqs.txt
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 11):
sys.exit("Dieses Skript braucht Python 3.11 oder neuer (tomllib). "
f"Aktuell: {sys.version.split()[0]}")
import argparse
import subprocess
import tempfile
import tomllib
from pathlib import Path
PROJECT_DIR = Path(__file__).resolve().parent
def resolve_group(groups: dict, name: str, seen: set[str] | None = None) -> list[str]:
"""Löst eine Dependency-Group inkl. {include-group = "..."} rekursiv auf."""
seen = set() if seen is None else seen
if name in seen:
raise ValueError(f"Zyklische include-group: {name}")
if name not in groups:
raise KeyError(f"Dependency-Group '{name}' nicht gefunden")
seen.add(name)
result: list[str] = []
for entry in groups[name]:
if isinstance(entry, str):
result.append(entry)
elif isinstance(entry, dict) and "include-group" in entry:
result.extend(resolve_group(groups, entry["include-group"], seen))
return result
def collect(data: dict, extras: list[str], groups: list[str], no_main: bool) -> list[str]:
project = data.get("project", {})
optional = project.get("optional-dependencies", {})
dep_groups = data.get("dependency-groups", {})
deps: list[str] = []
if not no_main:
deps.extend(project.get("dependencies", []))
for extra in extras:
if extra not in optional:
raise KeyError(f"Optional-Dependency '{extra}' nicht gefunden")
deps.extend(optional[extra])
for group in groups:
deps.extend(resolve_group(dep_groups, group))
# Duplikate entfernen, Reihenfolge behalten
return list(dict.fromkeys(d.strip() for d in deps if d.strip()))
def main() -> int:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("-p", "--pyproject", type=Path, default=PROJECT_DIR / "pyproject.toml",
help="Pfad zur pyproject.toml (Standard: neben diesem Skript)")
p.add_argument("--extra", action="append", default=None,
help="nur diese optional-dependencies (mehrfach möglich)")
p.add_argument("--group", action="append", default=None,
help="nur diese dependency-groups (mehrfach möglich)")
p.add_argument("--no-main", action="store_true", help="[project].dependencies weglassen")
p.add_argument("--list", action="store_true", help="Abhängigkeiten nur anzeigen")
p.add_argument("--write", action="store_true", help="requirements-Datei schreiben statt installieren")
p.add_argument("-o", "--output", type=Path, default=PROJECT_DIR / "requirements.txt",
help="Zieldatei für --write")
args = p.parse_args()
if not args.pyproject.exists():
print(f"{args.pyproject} nicht gefunden", file=sys.stderr)
return 1
data = tomllib.loads(args.pyproject.read_text(encoding="utf-8"))
all_extras = list(data.get("project", {}).get("optional-dependencies", {}))
all_groups = list(data.get("dependency-groups", {}))
# Ohne --extra/--group: alles. Mit einem davon: nur das Gewählte.
selective = args.extra is not None or args.group is not None
extras = (args.extra or []) if selective else all_extras
groups = (args.group or []) if selective else all_groups
try:
deps = collect(data, extras, groups, args.no_main)
except (KeyError, ValueError) as e:
print(f"Fehler: {e.args[0]}", file=sys.stderr)
print(f" Vorhandene --extra: {', '.join(all_extras) or '(keine)'}", file=sys.stderr)
print(f" Vorhandene --group: {', '.join(all_groups) or '(keine)'}", file=sys.stderr)
return 1
parts = ([] if args.no_main else ["Basis"]) + [f"extra:{e}" for e in extras] + [f"group:{g}" for g in groups]
print(f"Auswahl: {', '.join(parts) or '(nichts)'}")
if args.list:
print("\n".join(deps) or "(keine Abhängigkeiten)")
return 0
content = f"# Automatisch erzeugt aus {args.pyproject.name} – nicht von Hand bearbeiten\n" + "\n".join(deps) + "\n"
if args.write:
args.output.write_text(content, encoding="utf-8")
print(f"{len(deps)} Abhängigkeiten -> {args.output}")
return 0
if not deps:
print("Keine Abhängigkeiten gefunden – nichts zu tun.")
return 0
if sys.prefix == sys.base_prefix:
print("WARNUNG: Es ist kein virtuelles Environment aktiv – es wird in die globale "
f"Python-Installation installiert:\n {sys.prefix}", file=sys.stderr)
if sys.stdin.isatty() and input("Trotzdem fortfahren? [j/N] ").strip().lower() not in ("j", "ja", "y", "yes"):
return 1
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False, encoding="utf-8") as tmp:
tmp.write(content)
tmp_path = Path(tmp.name)
try:
print(f"Installiere {len(deps)} Abhängigkeiten in {sys.prefix}\n")
return subprocess.call([sys.executable, "-m", "pip", "install", "-r", str(tmp_path)])
finally:
tmp_path.unlink(missing_ok=True)
if __name__ == "__main__":
raise SystemExit(main())
+2
View File
@@ -2,3 +2,5 @@
name = "matrixmania" name = "matrixmania"
version = "0.1.0" version = "0.1.0"
dependencies = ["tabulate", "ruff"] dependencies = ["tabulate", "ruff"]
[project.optional-dependencies]
dev = ["pytest", "ruff", "pytest-cov"]
+25
View File
@@ -0,0 +1,25 @@
import pytest
from compute import project_ortho
def test_project_ortho_default_scale():
# Arrange
point = (1.0, 2.0, 3.0)
# Act
result = project_ortho(point)
# Assert
assert result == (1.0, 2.0)
def test_project_ortho_scale_100():
# Arrange
point = (1.0, 2.0, 3.0)
scale = 100.0
# Act
result = project_ortho(point, scale)
# Assert
assert result == (100.0, 200.0)
+30
View File
@@ -0,0 +1,30 @@
import pytest
from compute import rot_2D, rot_3D
def test_rot_2D_90_degrees():
# Arrange
angle = 90.0
# Act
matrix = rot_2D(angle)
# Assert (bei 90°: cos(90)=0, sin(90)=1)
assert pytest.approx(matrix[0][0], abs=1e-5) == 0.0
assert pytest.approx(matrix[0][1], abs=1e-5) == -1.0
assert pytest.approx(matrix[1][0], abs=1e-5) == 1.0
assert pytest.approx(matrix[1][1], abs=1e-5) == 0.0
def test_rot_3D_z_axis_90_degrees():
# Arrange
angle = 90.0
axis = 'z'
# Act
matrix = rot_3D(angle, axis)
# Assert
assert pytest.approx(matrix[0][0], abs=1e-5) == 0.0
assert pytest.approx(matrix[0][1], abs=1e-5) == -1.0
assert pytest.approx(matrix[2][2], abs=1e-5) == 1.0