Restructure the project and add tests

This commit is contained in:
tilo 2025-11-13 13:29:16 +01:00
parent a3897ab4d4
commit 6faa0d7456
12 changed files with 420 additions and 8 deletions

View File

@ -1,7 +1,6 @@
from tabulate import tabulate
from numeric.compute import matmul, transpose, rot_2D
from util.tools import substring
from src.matrixmania import matmul, rot_2D
if __name__ == '__main__':
# Substring

View File

@ -1 +0,0 @@
from .compute import matmul, transpose, rot_2D

View File

@ -1,6 +1,6 @@
[project]
name = "matrixmania_brockmannti"
version = "0.1.0"
version = "0.1.1"
description = "MatrixMania: Simple linear algebra functions for teaching (matmul, transpose, rot_2D)."
authors = [
{ name="Tilo Brockmann", email="brockmannti93157@th-nuernberg.de" }

View File

@ -0,0 +1 @@

View File

@ -40,7 +40,7 @@ def matmul(a: List[List[float]], b: List[List[float]]) -> List[List[float]]:
return result
def transpose(matrix: List[List[int]]) -> list[list[int]]:
def transpose(matrix: List[List[int]]) -> List[List[int]]:
"""
Transposes any given matrix
:param matrix: A nested list of integers describing a matrix
@ -63,10 +63,10 @@ def transpose(matrix: List[List[int]]) -> list[list[int]]:
return result
def rot_2D(angle) -> list[list[float]]:
def rot_2D(angle: float) -> List[List[float]]:
"""
Creates a rotation matrix from any given angle
:param angle: The angle (degrees)
Creates a two-dimensional rotation matrix from any given angle
:param angle: The angle (radians)
:return: The rotation matrix
:throws: Value error if the parsed angle is not of a numeric type
"""
@ -74,3 +74,42 @@ def rot_2D(angle) -> list[list[float]]:
raise ValueError("The parameter angle must be an instance of a numeric datatype")
return [[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]]
def rot_3D(angle: float, axis: str) -> List[List[float]]:
"""
Creates a three-dimensional rotation matrix from any given angle
:param angle: The angle (radians)
:param axis: The axis of rotation ("x", "y", or "z")
:return: The rotation matrix
:throws: Value error if parameters are of the wrong type or value
"""
if not isinstance(angle, (float, int)):
raise ValueError("The parameter angle must be an instance of a numeric datatype")
if not isinstance(axis, str):
raise ValueError("axis must be a string")
cos_val = math.cos(angle)
sin_val = math.sin(angle)
# Use '==' for string value comparison, not 'is'
if axis == "x":
return [
[1, 0, 0],
[0, cos_val, -sin_val],
[0, sin_val, cos_val] # This was 'math.sin(angle)'
]
elif axis == "y":
return [
[cos_val, 0, sin_val],
[0, 1, 0],
[-sin_val, 0, cos_val]
]
elif axis == "z":
return [
[cos_val, -sin_val, 0],
[sin_val, cos_val, 0],
[0, 0, 1]
]
else:
raise ValueError("axis must be either x, y or z")

View File

View File

@ -0,0 +1,87 @@
import math
from _pytest.python_api import approx
from src.matrixmania.compute import matmul
import pytest
def test_square_matrix_multiplication():
"""Tests the multiplication of two square matrices"""
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
expected = [[19, 22], [43, 50]]
assert matmul(a, b) == expected
def test_rectangular_matrix_multiplication():
"""Testet eine 2x3-Matrix multipliziert mit einer 3x2-Matrix."""
a = [[1, 2, 3], [4, 5, 6]] # 2x3
b = [[7, 8], [9, 10], [11, 12]] # 3x2
expected = [[58, 64], [139, 154]]
assert matmul(a, b) == expected
def test_matmul_string():
a = "[[1, 2], [3, 4]]"
b = "[[5, 6], [7, 8]]"
result = None
try:
matmul(a, b)
except ValueError:
result = ValueError
assert result == ValueError
def test_multiply_with_identity_matrix():
"""Testet die Multiplikation mit einer Einheitsmatrix (sollte die Matrix selbst zurückgeben)."""
a = [[5, 8, -2], [3, 0, 1], [1, 1, 4]]
identity = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
assert matmul(a, identity) == a
assert matmul(identity, a) == a
def test_vector_multiplication():
"""Testet einen Zeilenvektor (1x3) mal einen Spaltenvektor (3x1)."""
a = [[1, 2, 3]] # 1x3
b = [[4], [5], [6]] # 3x1
# Erwartetes Ergebnis (1x1 Matrix): [1*4 + 2*5 + 3*6] = [32]
expected = [[32]]
assert matmul(a, b) == expected
def test_floats_and_negatives():
"""Testet die Multiplikation mit Fließkommazahlen und negativen Werten."""
a = [[1.5, -2], [0, 3]]
b = [[4], [-0.5]] # 2x1
expected = [[7.0], [-1.5]]
result = matmul(a, b)
assert len(result) == len(expected)
assert len(result[0]) == len(expected[0])
for i in range(len(expected)):
for j in range(len(expected[0])):
assert math.isclose(result[i][j], expected[i][j], abs_tol=1e-9)
def test_dimension_mismatch_error():
"""Prüft, ob bei inkompatiblen Dimensionen ein ValueError ausgelöst wird."""
a = [[1, 2], [3, 4]] # 2x2
b = [[1], [2], [3]] # 3x1 (Spalten von A (2) != Zeilen von B (3))
# Wir erwarten einen ValueError mit der spezifischen Fehlermeldung
# Hinweis: Der Tippfehler "Breath" ist aus deinem Originalcode übernommen.
with pytest.raises(ValueError, match="Breath of first matrix must be equivalent to the height of the second"):
matmul(a, b)
def test_multiply_by_zero_matrix():
"""Testet die Multiplikation mit einer Nullmatrix."""
a = [[1, 2, 3], [4, 5, 6]]
b_zero = [[0, 0], [0, 0], [0, 0]]
expected = [[0, 0], [0, 0]]
assert matmul(a, b_zero) == expected

View File

@ -0,0 +1,93 @@
import math
import pytest
from src.matrixmania.compute import rot_2D
# --- HELPER FUNCTION ---
def assert_matrices_close(m1, m2, abs_tol=1e-9):
"""
Helper function to compare two 2x2 matrices element-wise
using math.isclose for float comparison.
"""
assert len(m1) == 2 and len(m1[0]) == 2, "Matrix 1 is not 2x2"
assert len(m2) == 2 and len(m2[0]) == 2, "Matrix 2 is not 2x2"
for i in range(2):
for j in range(2):
# Assert that each element is close within the tolerance
assert math.isclose(m1[i][j], m2[i][j], abs_tol=abs_tol), \
f"Mismatch at [{i}][{j}]: {m1[i][j]} != {m2[i][j]}"
def test_90_degree_rotation_in_radians():
"""Tests a 90-degree rotation (input is pi/2 radians)."""
# 90 degrees = pi / 2 radians
# cos(pi/2) = 0, sin(pi/2) = 1
# Expected: [[0, -1], [1, 0]]
expected = [[0.0, -1.0], [1.0, 0.0]]
# Use the helper function for comparison
assert_matrices_close(rot_2D(math.pi / 2), expected)
def test_0_radian_rotation():
"""Tests a 0-radian rotation (should return the identity matrix)."""
# cos(0) = 1, sin(0) = 0
# Expected: [[1, 0], [0, 1]]
expected = [[1.0, 0.0], [0.0, 1.0]]
assert_matrices_close(rot_2D(0), expected)
assert_matrices_close(rot_2D(0.0), expected)
def test_180_degree_rotation_in_radians():
"""Tests a 180-degree rotation (input is pi radians)."""
# 180 degrees = pi radians
# cos(pi) = -1, sin(pi) = 0
# Expected: [[-1, 0], [0, -1]]
expected = [[-1.0, 0.0], [0.0, -1.0]]
assert_matrices_close(rot_2D(math.pi), expected)
def test_360_degree_rotation_in_radians():
"""Tests a 360-degree rotation (input is 2*pi radians)."""
# 360 degrees = 2*pi radians
# cos(2*pi) = 1, sin(2*pi) = 0
expected = [[1.0, 0.0], [0.0, 1.0]]
assert_matrices_close(rot_2D(math.pi * 2), expected)
def test_negative_90_degree_rotation_in_radians():
"""Tests a clockwise 90-degree rotation (input is -pi/2 radians)."""
# -90 degrees = -pi/2 radians
# cos(-pi/2) = 0, sin(-pi/2) = -1
# Expected: [[0, 1], [-1, 0]]
expected = [[0.0, 1.0], [-1.0, 0.0]]
assert_matrices_close(rot_2D(-math.pi / 2), expected)
def test_45_degree_rotation_in_radians():
"""Tests a 45-degree rotation (input is pi/4 radians)."""
# 45 degrees = pi/4 radians
val = math.sqrt(2) / 2 # This is cos(pi/4) and sin(pi/4)
# Expected: [[val, -val], [val, val]]
expected = [[val, -val], [val, val]]
assert_matrices_close(rot_2D(math.pi / 4), expected)
# --- Tests for error handling (Invalid Inputs) ---
# (These tests remain the same)
def test_error_on_string_input():
"""Tests that a ValueError is raised for string input."""
with pytest.raises(ValueError, match="instance of a numeric datatype"):
rot_2D("90")
def test_error_on_list_input():
"""Tests that a ValueError is raised for list input."""
with pytest.raises(ValueError, match="instance of a numeric datatype"):
rot_2D([math.pi])
def test_error_on_none_input():
"""Tests that a ValueError is raised for None input."""
with pytest.raises(ValueError, match="instance of a numeric datatype"):
rot_2D(None)

View File

@ -0,0 +1,95 @@
# --- Tests for standard rotations (Happy Path) ---
import math
import pytest
from _pytest.python_api import approx
from src.matrixmania.compute import rot_3D
# --- HELPER FUNCTION ---
def assert_matrices_3x3_close(m1, m2, abs_tol=1e-9):
"""
Helper function to compare two 3x3 matrices element-wise
using math.isclose for float comparison.
"""
assert len(m1) == 3 and all(len(row) == 3 for row in m1), "Matrix 1 is not 3x3"
assert len(m2) == 3 and all(len(row) == 3 for row in m2), "Matrix 2 is not 3x3"
for i in range(3):
for j in range(3):
assert math.isclose(m1[i][j], m2[i][j], abs_tol=abs_tol), \
f"Mismatch at [{i}][{j}]: {m1[i][j]} != {m2[i][j]}"
# --- END HELPER FUNCTION ---
def test_90_deg_rotation_x_axis():
"""Tests a 90-degree (pi/2) rotation around the X-axis."""
# cos(pi/2) = 0, sin(pi/2) = 1
# Expected: [[1, 0, 0], [0, 0, -1], [0, 1, 0]]
expected = [[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]]
assert_matrices_3x3_close(rot_3D(math.pi / 2, "x"), expected)
def test_90_deg_rotation_y_axis():
"""Tests a 90-degree (pi/2) rotation around the Y-axis."""
# cos(pi/2) = 0, sin(pi/2) = 1
# Expected: [[0, 0, 1], [0, 1, 0], [-1, 0, 0]]
expected = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]]
assert_matrices_3x3_close(rot_3D(math.pi / 2, "y"), expected)
def test_90_deg_rotation_z_axis():
"""Tests a 90-degree (pi/2) rotation around the Z-axis."""
# cos(pi/2) = 0, sin(pi/2) = 1
# Expected: [[0, -1, 0], [1, 0, 0], [0, 0, 1]]
expected = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
assert_matrices_3x3_close(rot_3D(math.pi / 2, "z"), expected)
def test_0_radian_rotation():
"""Tests 0-radian rotation for all axes (should return identity matrix)."""
identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
assert_matrices_3x3_close(rot_3D(0, "x"), identity)
assert_matrices_3x3_close(rot_3D(0.0, "y"), identity)
assert_matrices_3x3_close(rot_3D(0, "z"), identity)
def test_pi_rotation_y_axis():
"""Tests a 180-degree (pi) rotation around Y."""
# cos(pi) = -1, sin(pi) = 0
# Expected: [[-1, 0, 0], [0, 1, 0], [0, 0, -1]]
expected = [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]]
assert_matrices_3x3_close(rot_3D(math.pi, "y"), expected)
# --- Tests for error handling (Invalid Inputs) ---
def test_error_invalid_axis_string():
"""Tests that a ValueError is raised for an invalid axis string."""
with pytest.raises(ValueError, match="axis must be either x, y or z"):
rot_3D(math.pi / 2, "a")
def test_error_invalid_axis_case():
"""Tests that the axis check is case-sensitive."""
with pytest.raises(ValueError, match="axis must be either x, y or z"):
rot_3D(math.pi / 2, "X") # 'X' is not 'x'
def test_error_invalid_axis_type():
"""Tests that a ValueError is raised if the axis is not a string."""
with pytest.raises(ValueError, match="axis must be a string"):
rot_3D(math.pi / 2, 1)
def test_error_invalid_angle_type():
"""Tests that a ValueError is raised if the angle is not numeric."""
with pytest.raises(ValueError, match="instance of a numeric datatype"):
rot_3D("pi", "x")
def test_error_invalid_angle_type_none():
"""Tests that a ValueError is raised if the angle is None."""
with pytest.raises(ValueError, match="instance of a numeric datatype"):
rot_3D(None, "z")

View File

@ -0,0 +1,99 @@
import pytest
from src.matrixmania.compute import transpose
def test_square_matrix():
"""Tests a standard 2x2 square matrix."""
matrix = [[1, 2], [3, 4]]
expected = [[1, 3], [2, 4]]
assert transpose(matrix) == expected
def test_rectangular_matrix_wide():
"""Tests a 2x3 rectangular matrix (more columns than rows)."""
matrix = [[1, 2, 3], [4, 5, 6]]
expected = [[1, 4], [2, 5], [3, 6]]
assert transpose(matrix) == expected
def test_rectangular_matrix_tall():
"""Tests a 3x2 rectangular matrix (more rows than columns)."""
matrix = [[1, 2], [3, 4], [5, 6]]
expected = [[1, 3, 5], [2, 4, 6]]
assert transpose(matrix) == expected
def test_row_vector():
"""Tests a single row matrix (1x4)."""
matrix = [[10, 20, 30, 40]]
expected = [[10], [20], [30], [40]]
assert transpose(matrix) == expected
def test_column_vector():
"""Tests a single column matrix (4x1)."""
matrix = [[10], [20], [30], [40]]
expected = [[10, 20, 30, 40]]
assert transpose(matrix) == expected
def test_single_element_matrix():
"""Tests a 1x1 matrix."""
matrix = [[-5]]
expected = [[-5]]
assert transpose(matrix) == expected
def test_identity_matrix():
"""Tests an identity matrix, which is its own transpose."""
matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
expected = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
assert transpose(matrix) == expected
# --- Error Handling Tests (Invalid Inputs) ---
def test_raises_error_if_not_list():
"""Tests that a ValueError is raised if the input is not a list (e.g., None)."""
with pytest.raises(ValueError, match="Argument needs to be a nested list of integers"):
transpose(None)
def test_raises_error_if_not_nested_list():
"""Tests that a ValueError is raised if the input is a flat list (not nested)."""
with pytest.raises(ValueError, match="Argument needs to be a nested list of integers"):
transpose([1, 2, 3])
def test_raises_error_if_not_list_of_int():
"""
Tests that a ValueError is raised if the matrix contains non-integers.
Note: The function's current check only validates the very first element (matrix[0][0]).
"""
with pytest.raises(ValueError, match="Argument needs to be a nested list of integers"):
transpose([[1.0, 2.0], [3.0, 4.0]])
def test_raises_error_on_empty_list_inner():
"""
Tests how the function handles a list with an empty inner list.
The check 'isinstance(matrix[0][0], int)' will fail with an IndexError.
The function *should* raise a ValueError, but it raises an IndexError due to brittle validation.
We test for the 'ValueError' that the *validation line itself* is supposed to catch.
Update: If the input is [[]], matrix[0] is [], and matrix[0][0] causes an IndexError.
This IndexError happens *before* the ValueError can be raised by the 'if' condition.
Therefore, we must test for the actual error that occurs.
"""
with pytest.raises(IndexError):
transpose([[]])
def test_raises_error_on_empty_list_outer():
"""
Tests how the function handles a completely empty list.
The check 'isinstance(matrix[0], list)' will fail with an IndexError.
"""
with pytest.raises(IndexError):
transpose([])