Skip to content

Commit 65b1482

Browse files
PixelCode01cclauss
andauthored
Add similarity test for square matrices (#13022)
* Add similarity test for square matrices * Fix naming convention: MatrixLike to matrix_like * Use TypeAlias for proper mypy type checking * Fix CI errors: use type keyword and fix line length * Fix docstring formatting in similar_matrices.py Corrected formatting and punctuation in docstring. * updating DIRECTORY.md --------- Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: cclauss <cclauss@users.noreply.github.com>
1 parent 1619583 commit 65b1482

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

‎DIRECTORY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@
10281028
* [Rotate Matrix](matrix/rotate_matrix.py)
10291029
* [Searching In Sorted Matrix](matrix/searching_in_sorted_matrix.py)
10301030
* [Sherman Morrison](matrix/sherman_morrison.py)
1031+
* [Similar Matrices](matrix/similar_matrices.py)
10311032
* [Spiral Print](matrix/spiral_print.py)
10321033
* Tests
10331034
* [Test Matrix Operation](matrix/tests/test_matrix_operation.py)

‎matrix/similar_matrices.py‎

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Determine whether two square matrices are similar.
2+
3+
Two square matrices :math:`A` and :math:`B` of the same size are similar if
4+
there exists an invertible matrix :math:`P` such that :math:`P^{-1} A P = B`.
5+
This implementation relies on SymPy to compute the Jordan canonical form of
6+
both matrices. Two matrices are similar precisely when their Jordan forms are
7+
equal up to permutation of the Jordan blocks.
8+
* https://en.wikipedia.org/wiki/Jordan_matrix
9+
* https://en.wikipedia.org/wiki/Jordan_normal_form
10+
11+
Examples
12+
--------
13+
>>> are_similar_matrices([[3, 1], [0, 3]], [[3, 0], [0, 3]])
14+
False
15+
>>> from sympy import Matrix
16+
>>> matrix_a = [[3, 1], [0, 3]]
17+
>>> transform = Matrix([[1, 1], [0, 1]])
18+
>>> matrix_b = (transform.inv() * Matrix(matrix_a) * transform).tolist()
19+
>>> are_similar_matrices(matrix_a, matrix_b)
20+
True
21+
>>> are_similar_matrices(
22+
... [[1, 2, 0], [0, 1, 0], [0, 0, 3]],
23+
... [[1, 0, 0], [0, 1, 0], [0, 0, 3]],
24+
... )
25+
False
26+
>>> are_similar_matrices([[1, 2], [0, 1]], [[1, 2, 0], [0, 1, 0]])
27+
Traceback (most recent call last):
28+
...
29+
ValueError: both matrices must be square with the same dimensions
30+
"""
31+
32+
from __future__ import annotations
33+
34+
from collections.abc import Sequence
35+
from typing import Any
36+
37+
from sympy import Matrix, nsimplify
38+
from sympy.matrices.common import MatrixError
39+
40+
__all__ = ["are_similar_matrices"]
41+
42+
43+
type MatrixLike = Sequence[Sequence[Any]] | Matrix
44+
45+
46+
def _as_square_matrix(matrix: MatrixLike, *, simplify_entries: bool) -> Matrix:
47+
"""Return a SymPy matrix after validating that ``matrix`` is square.
48+
49+
Parameters
50+
----------
51+
matrix:
52+
Nested sequences (or a SymPy matrix) describing the matrix entries.
53+
simplify_entries:
54+
When ``True`` each entry is passed through :func:`sympy.nsimplify`
55+
which helps treat values such as ``0.5`` and ``1/2`` as the same.
56+
57+
Raises
58+
------
59+
TypeError
60+
If ``matrix`` cannot be converted into a SymPy matrix.
61+
ValueError
62+
If ``matrix`` is not square.
63+
"""
64+
65+
try:
66+
sympy_matrix = Matrix(matrix)
67+
except (TypeError, ValueError) as exc: # pragma: no cover - defensive
68+
msg = "matrix input must be a rectangular sequence of numbers"
69+
raise TypeError(msg) from exc
70+
71+
if sympy_matrix.rows != sympy_matrix.cols:
72+
raise ValueError("both matrices must be square with the same dimensions")
73+
74+
if simplify_entries:
75+
sympy_matrix = sympy_matrix.applyfunc(nsimplify)
76+
77+
return sympy_matrix
78+
79+
80+
def _jordan_signature(matrix: Matrix) -> tuple[tuple[Any, tuple[int, ...]], ...]:
81+
"""Return a hashable representation of the Jordan form of ``matrix``."""
82+
83+
_, blocks = matrix.jordan_cells()
84+
summary: dict[Any, list[int]] = {}
85+
for block in blocks:
86+
block_matrix = Matrix(block)
87+
eigenvalue = block_matrix[0, 0]
88+
summary.setdefault(eigenvalue, []).append(block_matrix.rows)
89+
90+
return tuple(
91+
(
92+
eigenvalue,
93+
tuple(sorted(block_sizes, reverse=True)),
94+
)
95+
for eigenvalue, block_sizes in sorted(
96+
summary.items(), key=lambda item: repr(item[0])
97+
)
98+
)
99+
100+
101+
def are_similar_matrices(
102+
matrix_a: MatrixLike,
103+
matrix_b: MatrixLike,
104+
*,
105+
simplify_entries: bool = True,
106+
) -> bool:
107+
"""Return ``True`` if ``matrix_a`` and ``matrix_b`` are similar matrices.
108+
109+
Parameters
110+
----------
111+
matrix_a, matrix_b:
112+
Square matrices represented as nested sequences (or SymPy matrices).
113+
simplify_entries:
114+
If ``True`` (default), the function attempts to simplify each entry so
115+
that values that are algebraically equal are treated as such. Set this
116+
to ``False`` to skip simplification when working with symbolic inputs
117+
that should remain untouched.
118+
119+
Raises
120+
------
121+
ValueError
122+
If the matrices are not square or their dimensions do not match.
123+
TypeError
124+
If either matrix cannot be interpreted as a numeric matrix.
125+
"""
126+
127+
sympy_a = _as_square_matrix(matrix_a, simplify_entries=simplify_entries)
128+
sympy_b = _as_square_matrix(matrix_b, simplify_entries=simplify_entries)
129+
130+
if sympy_a.shape != sympy_b.shape:
131+
raise ValueError("both matrices must be square with the same dimensions")
132+
133+
try:
134+
signature_a = _jordan_signature(sympy_a)
135+
signature_b = _jordan_signature(sympy_b)
136+
except MatrixError as exc: # pragma: no cover - rare SymPy failure
137+
msg = "unable to determine the Jordan canonical form"
138+
raise ValueError(msg) from exc
139+
140+
return signature_a == signature_b
141+
142+
143+
if __name__ == "__main__": # pragma: no cover - convenience execution
144+
from doctest import testmod
145+
146+
testmod()

0 commit comments

Comments
 (0)