265 lines
7.7 KiB
Python
265 lines
7.7 KiB
Python
"""Provides matmul, transpose, and rotate functions for 2‑D numeric matrices."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from collections.abc import Iterable
|
||
from typing import Literal
|
||
|
||
from tabulate import tabulate
|
||
|
||
__all__ = ["matmul", "print_matrix", "rot_2d", "rotate", "transpose"]
|
||
|
||
_NUMERIC_TYPES = (int, float)
|
||
|
||
|
||
def _validate_matrix(
|
||
matrix: Iterable[Iterable[object]], name: str
|
||
) -> list[list[float]]:
|
||
"""Validate that *matrix* is a rectangular list of lists of numeric values.
|
||
|
||
Returns the matrix converted to ``list[list[float]]`` if valid.
|
||
Raises ``TypeError`` for non‑numeric values or if any element is a ``bool``.
|
||
Raises ``ValueError`` for empty matrices, empty rows, or ragged matrices.
|
||
"""
|
||
try:
|
||
rows = list(map(list, matrix))
|
||
except TypeError as exc:
|
||
raise TypeError(f"{name} must be a sequence of sequences") from exc
|
||
|
||
if not rows:
|
||
raise ValueError(f"{name} must not be empty")
|
||
|
||
if not rows[0]:
|
||
raise ValueError(f"{name} must not contain empty rows")
|
||
|
||
row_len = len(rows[0])
|
||
for row in rows:
|
||
if len(row) != row_len:
|
||
raise ValueError(f"{name} must be rectangular (row lengths differ)")
|
||
for value in row:
|
||
if isinstance(value, bool):
|
||
raise TypeError(f"{name} contains boolean value {value!r}")
|
||
if not isinstance(value, _NUMERIC_TYPES):
|
||
raise TypeError(f"{name} contains non‑numeric value {value!r}")
|
||
return [[float(value) for value in row] for row in rows]
|
||
|
||
|
||
def matmul(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
|
||
"""Multiply two matrices ``a`` and ``b``.
|
||
|
||
Parameters
|
||
----------
|
||
a: list[list[float]]
|
||
Left operand matrix.
|
||
b: list[list[float]]
|
||
Right operand matrix.
|
||
|
||
Returns
|
||
-------
|
||
list[list[float]]
|
||
The product matrix.
|
||
|
||
Raises
|
||
------
|
||
ValueError
|
||
If matrix dimensions are incompatible or if a matrix is ragged.
|
||
TypeError
|
||
If inputs are not sequences of numeric values.
|
||
"""
|
||
a_matrix = _validate_matrix(a, "a")
|
||
b_matrix = _validate_matrix(b, "b")
|
||
|
||
num_rows_a = len(a_matrix)
|
||
num_cols_a = len(a_matrix[0])
|
||
num_rows_b = len(b_matrix)
|
||
num_cols_b = len(b_matrix[0])
|
||
|
||
if num_cols_a != num_rows_b:
|
||
raise ValueError(
|
||
f"Incompatible dimensions: a has {num_cols_a} columns; "
|
||
f"b has {num_rows_b} rows"
|
||
)
|
||
|
||
result: list[list[float]] = [
|
||
[0.0 for _ in range(num_cols_b)] for _ in range(num_rows_a)
|
||
]
|
||
|
||
for i in range(num_rows_a):
|
||
for j in range(num_cols_b):
|
||
dot_product = 0.0
|
||
for k in range(num_cols_a):
|
||
dot_product += a_matrix[i][k] * b_matrix[k][j]
|
||
result[i][j] = dot_product
|
||
return result
|
||
|
||
|
||
def transpose(matrix: list[list[float]]) -> list[list[float]]:
|
||
"""Return the transpose of *matrix*.
|
||
|
||
Parameters
|
||
----------
|
||
matrix: list[list[float]]
|
||
Input matrix as a list of rows.
|
||
|
||
Returns
|
||
-------
|
||
list[list[float]]
|
||
Transposed matrix.
|
||
|
||
Raises
|
||
------
|
||
TypeError, ValueError
|
||
Propagated from :func:`_validate_matrix` when the input is not a
|
||
rectangular numeric matrix.
|
||
"""
|
||
m_matrix = _validate_matrix(matrix, "matrix")
|
||
rows, cols = len(m_matrix), len(m_matrix[0])
|
||
return [[m_matrix[i][j] for i in range(rows)] for j in range(cols)]
|
||
|
||
|
||
def rotate(matrix: list[list[float]]) -> list[list[float]]:
|
||
"""Return the matrix rotated 90° clockwise.
|
||
|
||
Parameters
|
||
----------
|
||
matrix: list[list[float]]
|
||
Input matrix as a list of rows.
|
||
|
||
Returns
|
||
-------
|
||
list[list[float]]
|
||
Rotated matrix.
|
||
|
||
Raises
|
||
------
|
||
TypeError, ValueError
|
||
Propagated from :func:`_validate_matrix` when the input is not a
|
||
rectangular numeric matrix.
|
||
"""
|
||
m_matrix = _validate_matrix(matrix, "matrix")
|
||
return [list(row) for row in zip(*reversed(m_matrix))]
|
||
|
||
|
||
def rot_2d(angle: float, unit: Literal["deg", "rad"] = "deg") -> list[list[float]]:
|
||
"""Return a 2×2 rotation matrix for a given angle.
|
||
|
||
Parameters
|
||
----------
|
||
angle : float
|
||
Rotation angle.
|
||
unit : {"deg", "rad"}, default "deg"
|
||
Whether *angle* is given in degrees or radians.
|
||
|
||
Returns
|
||
-------
|
||
list[list[float]]
|
||
2×2 rotation matrix.
|
||
"""
|
||
theta = math.radians(angle) if unit == "deg" else angle
|
||
c = math.cos(theta)
|
||
s = math.sin(theta)
|
||
return [[c, -s], [s, c]]
|
||
|
||
|
||
def print_matrix(title: str, matrix: list[list[float]]) -> None:
|
||
"""Print a title followed by each matrix row on its own line."""
|
||
print(title)
|
||
for row in matrix:
|
||
print(" ", row)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Matrix multiplication tests
|
||
a1 = [[1, 2, 3], [4, 5, 6]]
|
||
b1 = [[7, 8], [9, 10], [11, 12]]
|
||
print_matrix("Matrix A:", a1)
|
||
print_matrix("Matrix B:", b1)
|
||
product = matmul(a1, b1)
|
||
print_matrix("A x B:", product)
|
||
assert product == [[58, 64], [139, 154]], "matmul first test failed"
|
||
print()
|
||
|
||
a2 = [[1, 2], [3, 4]]
|
||
b2 = [[5, 6], [7, 8]]
|
||
print_matrix("Matrix A:", a2)
|
||
print_matrix("Matrix B:", b2)
|
||
product = matmul(a2, b2)
|
||
print_matrix("A x B:", product)
|
||
assert product == [[19, 22], [43, 50]], "matmul second test failed"
|
||
print()
|
||
|
||
identity = [[1, 0], [0, 1]]
|
||
print_matrix("Matrix A:", a2)
|
||
print_matrix("Identity matrix:", identity)
|
||
product = matmul(a2, identity)
|
||
print_matrix("A x Identity:", product)
|
||
assert product == a2, "matmul identity test failed"
|
||
print()
|
||
|
||
# Dimension mismatch must raise ValueError
|
||
try:
|
||
matmul([[1, 2]], [[1, 2]])
|
||
except ValueError as error:
|
||
print(f"Expected error for mismatched dimensions: {error}")
|
||
else:
|
||
raise AssertionError("matmul did not raise ValueError")
|
||
print()
|
||
|
||
# Transpose tests
|
||
print_matrix("Matrix:", a1)
|
||
transposed = transpose(a1)
|
||
print_matrix("Transposed:", transposed)
|
||
assert transposed == [[1, 4], [2, 5], [3, 6]], "transpose first test failed"
|
||
assert transpose(transposed) == a1, "double transpose failed"
|
||
print()
|
||
|
||
column = [[1], [2], [3]]
|
||
print_matrix("Matrix:", column)
|
||
transposed = transpose(column)
|
||
print_matrix("Transposed:", transposed)
|
||
assert transposed == [[1, 2, 3]], "transpose column test failed"
|
||
print()
|
||
|
||
# Rotation tests
|
||
print_matrix("Matrix:", a1)
|
||
rotated = rotate(a1)
|
||
print_matrix("Rotated 90 degrees clockwise:", rotated)
|
||
assert rotated == [[4, 1], [5, 2], [6, 3]], "rotate 2x3 test failed"
|
||
print()
|
||
|
||
square = [[1, 2], [3, 4]]
|
||
print_matrix("Matrix:", square)
|
||
rotated = rotate(square)
|
||
print_matrix("Rotated 90 degrees clockwise:", rotated)
|
||
assert rotated == [[3, 1], [4, 2]], "rotate square test failed"
|
||
print()
|
||
|
||
# Four rotations must return the original matrix
|
||
rotated = square
|
||
for _ in range(4):
|
||
rotated = rotate(rotated)
|
||
print_matrix("Square matrix rotated four times:", rotated)
|
||
assert rotated == square, "four rotations did not return original"
|
||
print()
|
||
|
||
# 2D rotation matrix tests
|
||
rotation_0 = rot_2d(0)
|
||
print_matrix("Rotation matrix for 0 degrees:", rotation_0)
|
||
assert rotation_0 == [[1.0, -0.0], [0.0, 1.0]], "rot_2d 0 degrees failed"
|
||
print()
|
||
|
||
rotation_90 = rot_2d(90)
|
||
print_matrix("Rotation matrix for 90 degrees:", rotation_90)
|
||
point = [[1], [0]]
|
||
rotated_point = matmul(rotation_90, point)
|
||
print_matrix("Point (1, 0) rotated by 90 degrees:", rotated_point)
|
||
assert math.isclose(rotated_point[0][0], 0.0, abs_tol=1e-9), "rot_2d x failed"
|
||
assert math.isclose(rotated_point[1][0], 1.0, abs_tol=1e-9), "rot_2d y failed"
|
||
print()
|
||
|
||
print("All tests passed.")
|
||
# This is a test commit
|
||
print("This is a test")
|
||
print(tabulate(rot_2d(1.0)))
|