Restructure the project and add tests
This commit is contained in:
parent
a3897ab4d4
commit
6faa0d7456
3
main.py
3
main.py
@ -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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
from .compute import matmul, transpose, rot_2D
|
||||
@ -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" }
|
||||
|
||||
1
src/matrixmania/__init__.py
Normal file
1
src/matrixmania/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -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")
|
||||
0
test/test_matrixmania/__init__.py
Normal file
0
test/test_matrixmania/__init__.py
Normal file
87
test/test_matrixmania/test_matmul.py
Normal file
87
test/test_matrixmania/test_matmul.py
Normal 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
|
||||
93
test/test_matrixmania/test_rot_2D.py
Normal file
93
test/test_matrixmania/test_rot_2D.py
Normal 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)
|
||||
95
test/test_matrixmania/test_rot_3D.py
Normal file
95
test/test_matrixmania/test_rot_3D.py
Normal 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")
|
||||
99
test/test_matrixmania/test_transpose.py
Normal file
99
test/test_matrixmania/test_transpose.py
Normal 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([])
|
||||
Loading…
x
Reference in New Issue
Block a user