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
+255 -80

No files matched your search

+51 -79
View File
@@ -1,8 +1,7 @@
"""Compute operations for matrices.
This module provides public functions for matrix multiplication, transposition,
matrix rotation, and generating 2D rotation matrices. The implementation is
lightweight, well typed and contains detailed Google‑style documentation.
matrix rotation, and generating 2D and 3D rotation matrices as well as projections.
"""
from __future__ import annotations
@@ -12,7 +11,7 @@ from typing import List
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]]:
"""Multiply two 2D matrices.
@@ -21,33 +20,25 @@ def matmul(
rows in ``matrix_b``.
Args:
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,
each a list of p floats.
matrix_a: A matrix of shape (m, n) represented as a list of m rows.
matrix_b: A matrix of shape (n, p) represented as a list of n rows.
Returns:
A new matrix of shape (m, p) containing the product
``matrix_a @ matrix_b``.
A new matrix of shape (m, p) containing the product ``matrix_a @ matrix_b``.
Raises:
ValueError: If either matrix is empty or if the column count of
``matrix_a`` does not match the row count of ``matrix_b``.
ValueError: If either matrix is empty or dimensions do not match.
"""
# SCHRITT 1: Validierung – Prüfen, ob die Matrizen überhaupt Daten enthalten
if not matrix_a or not matrix_b:
raise ValueError("Input matrices must not be empty.")
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):
raise ValueError("All rows of matrix_a must have the same number of columns.")
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:
raise ValueError(
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])
# SCHRITT 4: Konsistenzprüfung für 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.")
# SCHRITT 5: Eigentliche Matrixmultiplikation (Skalarprodukt der Zeilen und Spalten)
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] = []
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))
row.append(elem)
result.append(row)
@@ -72,18 +61,7 @@ def matmul(
def transpose(matrix: List[List[float]]) -> List[List[float]]:
"""Return the transpose of a 2‑D 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.
"""
"""Return the transpose of a 2D matrix."""
if not 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]]:
"""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.
"""
"""Rotate a square matrix 90 degrees clockwise."""
if not matrix:
raise ValueError("Cannot rotate an empty matrix.")
n_rows = len(matrix)
n_cols = len(matrix[0])
if any(len(row) != n_cols for row in matrix):
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]]:
"""Return the 2D rotation matrix for a given angle.
Calculates the 2x2 rotation matrix used in computer graphics to rotate a point
counterclockwise around the origin by angle theta (in radians).
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 the 2D rotation matrix for a given angle in degrees."""
rad = math.radians(theta)
c = math.cos(rad)
s = math.sin(rad)
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__":
# Testdaten für die Demonstration
a = [[1, 2, 3], [4, 5, 6]]
b = [[7, 8], [9, 10], [11, 12]]
print("matmul(a, b):")
print(matmul(a, b))
print("matmul(a, b):", matmul(a, b))
print("transpose(a):", transpose(a))
print("transpose(a):")
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])
# Teste deine Tests mit: python -m pytest
+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())
+3 -1
View File
@@ -1,4 +1,6 @@
[project]
name = "matrixmania"
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