Files

265 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)))