172 lines
4.6 KiB
Python
172 lines
4.6 KiB
Python
|
||
|
||
"""Utility functions for matrix operations.
|
||
|
||
This module contains:
|
||
* ``matmul`` – standard matrix multiplication.
|
||
* ``transpose`` – row↔column swapping.
|
||
* ``rotate`` – 90‑degree clockwise rotations.
|
||
|
||
All functions use type hints and raise informative errors where
|
||
dimensions are incompatible or matrices are non‑rectangular.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from tabulate import tabulate
|
||
|
||
Matrix = list[list[int | float]]
|
||
|
||
|
||
def matmul(a: Matrix, b: Matrix) -> Matrix:
|
||
"""Return the matrix product a @ b.
|
||
|
||
Parameters
|
||
----------
|
||
a : Matrix
|
||
Left‑hand matrix.
|
||
b : Matrix
|
||
Right‑hand matrix.
|
||
|
||
Returns
|
||
-------
|
||
Matrix
|
||
Product matrix.
|
||
|
||
Raises
|
||
------
|
||
ValueError
|
||
If matrices are not rectangular or dimensions are incompatible.
|
||
"""
|
||
def _rect(mat: Matrix) -> bool:
|
||
return all(len(row) == len(mat[0]) for row in mat)
|
||
|
||
if not a or not b:
|
||
raise ValueError("matrices may not be empty")
|
||
if not _rect(a) or not _rect(b):
|
||
raise ValueError("input matrices must be rectangular")
|
||
|
||
r1, c1 = len(a), len(a[0])
|
||
r2, c2 = len(b), len(b[0])
|
||
if c1 != r2:
|
||
raise ValueError("first matrix columns must equal second matrix rows")
|
||
|
||
result: Matrix = [[0 for _ in range(c2)] for _ in range(r1)]
|
||
for i in range(r1):
|
||
for j in range(c2):
|
||
s = 0
|
||
for k in range(c1):
|
||
s += a[i][k] * b[k][j]
|
||
result[i][j] = s
|
||
return result
|
||
|
||
|
||
def transpose(matrix: Matrix) -> Matrix:
|
||
"""Return the transpose of a 2‑D matrix.
|
||
|
||
The transpose operation swaps rows and columns: each row in the
|
||
result is a column from the input matrix.
|
||
|
||
Parameters
|
||
----------
|
||
matrix : Matrix
|
||
Two‑dimensional list of numbers. All rows must contain the
|
||
same number of columns.
|
||
|
||
Returns
|
||
-------
|
||
Matrix
|
||
Transposed matrix.
|
||
|
||
Raises
|
||
------
|
||
ValueError
|
||
If *matrix* is empty or if the rows have differing lengths.
|
||
"""
|
||
if not matrix:
|
||
raise ValueError("transpose requires a non‑empty matrix")
|
||
if any(len(row) != len(matrix[0]) for row in matrix):
|
||
raise ValueError("matrix must be rectangular to transpose")
|
||
rows, cols = len(matrix), len(matrix[0])
|
||
return [[matrix[i][j] for i in range(rows)] for j in range(cols)]
|
||
|
||
|
||
def rot_2D(theta: float) -> list[list[float]]:
|
||
"""Return the 2‑D rotation matrix for angle *theta* in degrees.
|
||
|
||
The matrix is
|
||
``[[cos(θ), -sin(θ)],
|
||
[sin(θ), cos(θ)]]``
|
||
where θ is interpreted as a clockwise rotation.
|
||
|
||
Parameters
|
||
----------
|
||
theta : float
|
||
Rotation angle in degrees (clockwise).
|
||
|
||
Returns
|
||
-------
|
||
list[list[float]]
|
||
2x2 rotation matrix.
|
||
"""
|
||
import math
|
||
rad = math.radians(theta)
|
||
cos_t = math.cos(rad)
|
||
sin_t = math.sin(rad)
|
||
return [[cos_t, -sin_t], [sin_t, cos_t]]
|
||
|
||
|
||
def rotate(matrix: Matrix, degrees: float) -> Matrix:
|
||
"""Rotate the matrix clockwise by 0, 90, 180, or 270 degrees.
|
||
|
||
Parameters
|
||
----------
|
||
matrix : Matrix
|
||
degrees : float
|
||
Must be one of 0, 90, 180, 270.
|
||
|
||
Returns
|
||
-------
|
||
Matrix
|
||
|
||
Raises
|
||
------
|
||
ValueError
|
||
If *degrees* is unsupported.
|
||
"""
|
||
if degrees not in (0, 90, 180, 270):
|
||
raise ValueError("degrees must be one of 0, 90, 180, 270")
|
||
if not matrix:
|
||
return []
|
||
n_rows, n_cols = len(matrix), len(matrix[0])
|
||
if degrees == 0:
|
||
return [row[:] for row in matrix]
|
||
if degrees == 90:
|
||
return [[matrix[n_rows - 1 - j][i] for j in range(n_rows)] for i in range(n_cols)]
|
||
if degrees == 180:
|
||
return [[matrix[n_rows - 1 - i][n_cols - 1 - j] for j in range(n_cols)] for i in range(n_rows)]
|
||
# 270
|
||
return [[matrix[j][n_cols - 1 - i] for j in range(n_rows)] for i in range(n_cols)]
|
||
|
||
if __name__ == "__main__":
|
||
# matmul tests
|
||
a1 = [[1, 2], [3, 4]]
|
||
b1 = [[5, 6], [7, 8]]
|
||
assert matmul(a1, b1) == [[19, 22], [43, 50]], "matmul example 1 failed"
|
||
a2 = [[1], [2], [3]]
|
||
b2 = [[4, 5, 6]]
|
||
assert matmul(a2, b2) == [[4, 5, 6], [8, 10, 12], [12, 15, 18]], "matmul example 2 failed"
|
||
|
||
# transpose test
|
||
mat = [[1, 2, 3], [4, 5, 6]]
|
||
assert transpose(mat) == [[1, 4], [2, 5], [3, 6]], "transpose failed"
|
||
|
||
# rotation tests
|
||
mat3 = [[1, 2], [3, 4], [5, 6]]
|
||
assert rotate(mat3, 90) == [[5, 3, 1], [6, 4, 2]], "rotate 90 failed"
|
||
assert rotate(mat3, 180) == [[6, 5], [4, 3], [2, 1]], "rotate 180 failed"
|
||
assert rotate(mat3, 270) == [[2, 4, 6], [1, 3, 5]], "rotate 270 failed"
|
||
assert rotate(mat3, 0) == mat3, "rotate 0 failed"
|
||
|
||
print(tabulate(rot_2D(1.0)))
|