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