GrokLA 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grokla-0.1.0/LICENSE +0 -0
- grokla-0.1.0/PKG-INFO +19 -0
- grokla-0.1.0/README.md +1 -0
- grokla-0.1.0/pyproject.toml +34 -0
- grokla-0.1.0/setup.cfg +4 -0
- grokla-0.1.0/src/GrokLA.egg-info/PKG-INFO +19 -0
- grokla-0.1.0/src/GrokLA.egg-info/SOURCES.txt +11 -0
- grokla-0.1.0/src/GrokLA.egg-info/dependency_links.txt +1 -0
- grokla-0.1.0/src/GrokLA.egg-info/top_level.txt +1 -0
- grokla-0.1.0/src/pymatrix/__init__.py +3 -0
- grokla-0.1.0/src/pymatrix/matrix.py +638 -0
- grokla-0.1.0/src/pymatrix/py.typed +0 -0
- grokla-0.1.0/tests/test_basic.py +19 -0
grokla-0.1.0/LICENSE
ADDED
|
File without changes
|
grokla-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: GrokLA
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python linear algebra Matrix class (no third-party dependencies).
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourname/pymatrix
|
|
8
|
+
Project-URL: Repository, https://github.com/yourname/pymatrix
|
|
9
|
+
Project-URL: Issues, https://github.com/yourname/pymatrix/issues
|
|
10
|
+
Keywords: linear-algebra,matrix,linalg
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
这是一个纯 Python 实现的线性代数矩阵库
|
grokla-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
这是一个纯 Python 实现的线性代数矩阵库
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "GrokLA" # 注意:发布到 PyPI 时必须唯一,若已被占用就换名
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pure-Python linear algebra Matrix class (no third-party dependencies)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Your Name" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["linear-algebra", "matrix", "linalg"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/yourname/pymatrix"
|
|
24
|
+
Repository = "https://github.com/yourname/pymatrix"
|
|
25
|
+
Issues = "https://github.com/yourname/pymatrix/issues"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = {"" = "src"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
pymatrix = ["py.typed"]
|
grokla-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: GrokLA
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python linear algebra Matrix class (no third-party dependencies).
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourname/pymatrix
|
|
8
|
+
Project-URL: Repository, https://github.com/yourname/pymatrix
|
|
9
|
+
Project-URL: Issues, https://github.com/yourname/pymatrix/issues
|
|
10
|
+
Keywords: linear-algebra,matrix,linalg
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
这是一个纯 Python 实现的线性代数矩阵库
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/GrokLA.egg-info/PKG-INFO
|
|
5
|
+
src/GrokLA.egg-info/SOURCES.txt
|
|
6
|
+
src/GrokLA.egg-info/dependency_links.txt
|
|
7
|
+
src/GrokLA.egg-info/top_level.txt
|
|
8
|
+
src/pymatrix/__init__.py
|
|
9
|
+
src/pymatrix/matrix.py
|
|
10
|
+
src/pymatrix/py.typed
|
|
11
|
+
tests/test_basic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymatrix
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from numbers import Real
|
|
5
|
+
from typing import Iterable, List, Sequence, Tuple, Union, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Number = Union[int, float]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Matrix:
|
|
12
|
+
"""
|
|
13
|
+
A pure-Python 2D matrix class for core linear algebra operations.
|
|
14
|
+
|
|
15
|
+
Design notes:
|
|
16
|
+
- Zero third-party dependencies (no numpy/scipy).
|
|
17
|
+
- Floating-point tolerance (epsilon) is used for near-zero and equality checks.
|
|
18
|
+
- Operations like transpose()/inv() return new Matrix instances (immutability-friendly).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
epsilon: float = 1e-7
|
|
22
|
+
|
|
23
|
+
def __init__(self, data: Sequence[Sequence[Real]]):
|
|
24
|
+
"""
|
|
25
|
+
Initialize a Matrix from a rectangular 2D sequence.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
data: A 2D sequence (e.g., list of lists). Each row must have the same length.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If rows have inconsistent lengths, or data is empty/ill-formed.
|
|
32
|
+
TypeError: If any element cannot be converted to float.
|
|
33
|
+
"""
|
|
34
|
+
if not isinstance(data, Sequence) or len(data) == 0:
|
|
35
|
+
raise ValueError("data must be a non-empty 2D sequence")
|
|
36
|
+
|
|
37
|
+
rows: List[List[float]] = []
|
|
38
|
+
rowlen: Optional[int] = None
|
|
39
|
+
|
|
40
|
+
for r, row in enumerate(data):
|
|
41
|
+
if not isinstance(row, Sequence) or len(row) == 0:
|
|
42
|
+
raise ValueError("each row must be a non-empty sequence")
|
|
43
|
+
if rowlen is None:
|
|
44
|
+
rowlen = len(row)
|
|
45
|
+
elif len(row) != rowlen:
|
|
46
|
+
raise ValueError("all rows must have the same length")
|
|
47
|
+
|
|
48
|
+
newrow: List[float] = []
|
|
49
|
+
for c, x in enumerate(row):
|
|
50
|
+
try:
|
|
51
|
+
newrow.append(float(x))
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise TypeError(f"matrix element at ({r},{c}) is not numeric") from e
|
|
54
|
+
rows.append(newrow)
|
|
55
|
+
|
|
56
|
+
self._data: List[List[float]] = rows
|
|
57
|
+
|
|
58
|
+
# -------------------------
|
|
59
|
+
# Magic methods
|
|
60
|
+
# -------------------------
|
|
61
|
+
|
|
62
|
+
def __add__(self, other: "Matrix") -> "Matrix":
|
|
63
|
+
"""A + B -> calls add()."""
|
|
64
|
+
return self.add(other)
|
|
65
|
+
|
|
66
|
+
def __sub__(self, other: "Matrix") -> "Matrix":
|
|
67
|
+
"""A - B -> calls sub()."""
|
|
68
|
+
return self.sub(other)
|
|
69
|
+
|
|
70
|
+
def __mul__(self, other: Union["Matrix", Real]) -> "Matrix":
|
|
71
|
+
"""
|
|
72
|
+
A * k (scalar) -> calls scale()
|
|
73
|
+
A * B (matrix) -> calls dot()
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(other, Matrix):
|
|
76
|
+
return self.dot(other)
|
|
77
|
+
if isinstance(other, Real):
|
|
78
|
+
return self.scale(float(other))
|
|
79
|
+
raise TypeError("unsupported operand type(s) for *: 'Matrix' and '{}'".format(type(other).__name__))
|
|
80
|
+
|
|
81
|
+
def __rmul__(self, other: Real) -> "Matrix":
|
|
82
|
+
"""k * A -> scalar scaling."""
|
|
83
|
+
if isinstance(other, Real):
|
|
84
|
+
return self.scale(float(other))
|
|
85
|
+
raise TypeError("unsupported operand type(s) for *: '{}' and 'Matrix'".format(type(other).__name__))
|
|
86
|
+
|
|
87
|
+
def __matmul__(self, other: "Matrix") -> "Matrix":
|
|
88
|
+
"""A @ B -> calls dot()."""
|
|
89
|
+
return self.dot(other)
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
m, n = self.shape()
|
|
93
|
+
return f"Matrix({m}x{n}, data={self._data})"
|
|
94
|
+
|
|
95
|
+
# -------------------------
|
|
96
|
+
# Properties & generation
|
|
97
|
+
# -------------------------
|
|
98
|
+
|
|
99
|
+
def shape(self) -> Tuple[int, int]:
|
|
100
|
+
"""
|
|
101
|
+
Return (m, n) where m is row count and n is column count.
|
|
102
|
+
"""
|
|
103
|
+
return (len(self._data), len(self._data[0]))
|
|
104
|
+
|
|
105
|
+
def clone(self) -> "Matrix":
|
|
106
|
+
"""
|
|
107
|
+
Return a deep copy of this matrix.
|
|
108
|
+
"""
|
|
109
|
+
return Matrix(deepcopy(self._data))
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def eye(cls, n: int) -> "Matrix":
|
|
113
|
+
"""
|
|
114
|
+
Create an n x n identity matrix.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
n: Size of the identity matrix.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If n <= 0.
|
|
121
|
+
"""
|
|
122
|
+
if n <= 0:
|
|
123
|
+
raise ValueError("n must be positive")
|
|
124
|
+
data = [[0.0] * n for _ in range(n)]
|
|
125
|
+
for i in range(n):
|
|
126
|
+
data[i][i] = 1.0
|
|
127
|
+
return cls(data)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def zeros(cls, m: int, n: int) -> "Matrix":
|
|
131
|
+
"""
|
|
132
|
+
Create an m x n zero matrix.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
m: Row count.
|
|
136
|
+
n: Column count.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If m <= 0 or n <= 0.
|
|
140
|
+
"""
|
|
141
|
+
if m <= 0 or n <= 0:
|
|
142
|
+
raise ValueError("m and n must be positive")
|
|
143
|
+
return cls([[0.0] * n for _ in range(m)])
|
|
144
|
+
|
|
145
|
+
# -------------------------
|
|
146
|
+
# Basic operations
|
|
147
|
+
# -------------------------
|
|
148
|
+
|
|
149
|
+
def add(self, other: "Matrix") -> "Matrix":
|
|
150
|
+
"""
|
|
151
|
+
Element-wise matrix addition.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
other: Another matrix with the same shape.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If shapes do not match.
|
|
158
|
+
"""
|
|
159
|
+
self._checkshape(other)
|
|
160
|
+
m, n = self.shape()
|
|
161
|
+
out = [[self._data[i][j] + other._data[i][j] for j in range(n)] for i in range(m)]
|
|
162
|
+
return Matrix(out)
|
|
163
|
+
|
|
164
|
+
def sub(self, other: "Matrix") -> "Matrix":
|
|
165
|
+
"""
|
|
166
|
+
Element-wise matrix subtraction.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
other: Another matrix with the same shape.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: If shapes do not match.
|
|
173
|
+
"""
|
|
174
|
+
self._checkshape(other)
|
|
175
|
+
m, n = self.shape()
|
|
176
|
+
out = [[self._data[i][j] - other._data[i][j] for j in range(n)] for i in range(m)]
|
|
177
|
+
return Matrix(out)
|
|
178
|
+
|
|
179
|
+
def scale(self, k: Real) -> "Matrix":
|
|
180
|
+
"""
|
|
181
|
+
Scalar multiplication: k * A.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
k: Scalar multiplier.
|
|
185
|
+
"""
|
|
186
|
+
kk = float(k)
|
|
187
|
+
m, n = self.shape()
|
|
188
|
+
out = [[self._data[i][j] * kk for j in range(n)] for i in range(m)]
|
|
189
|
+
return Matrix(out)
|
|
190
|
+
|
|
191
|
+
def dot(self, other: "Matrix") -> "Matrix":
|
|
192
|
+
"""
|
|
193
|
+
Matrix multiplication: A x B.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
other: Right-hand matrix.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: If inner dimensions do not match.
|
|
200
|
+
"""
|
|
201
|
+
m, n = self.shape()
|
|
202
|
+
p, q = other.shape()
|
|
203
|
+
if n != p:
|
|
204
|
+
raise ValueError(f"dimension mismatch: {m}x{n} cannot dot {p}x{q}")
|
|
205
|
+
|
|
206
|
+
out = [[0.0] * q for _ in range(m)]
|
|
207
|
+
# Classic triple loop (with a small optimization by reusing row/col access)
|
|
208
|
+
for i in range(m):
|
|
209
|
+
row = self._data[i]
|
|
210
|
+
for k in range(n):
|
|
211
|
+
aik = row[k]
|
|
212
|
+
if abs(aik) <= self.epsilon:
|
|
213
|
+
continue
|
|
214
|
+
ok = other._data[k]
|
|
215
|
+
for j in range(q):
|
|
216
|
+
out[i][j] += aik * ok[j]
|
|
217
|
+
return Matrix(out)
|
|
218
|
+
|
|
219
|
+
# -------------------------
|
|
220
|
+
# Transformations & features
|
|
221
|
+
# -------------------------
|
|
222
|
+
|
|
223
|
+
def transpose(self) -> "Matrix":
|
|
224
|
+
"""
|
|
225
|
+
Return the transpose matrix A^T.
|
|
226
|
+
"""
|
|
227
|
+
m, n = self.shape()
|
|
228
|
+
out = [[self._data[i][j] for i in range(m)] for j in range(n)]
|
|
229
|
+
return Matrix(out)
|
|
230
|
+
|
|
231
|
+
def trace(self) -> float:
|
|
232
|
+
"""
|
|
233
|
+
Return the trace of a square matrix (sum of diagonal elements).
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
ValueError: If the matrix is not square.
|
|
237
|
+
"""
|
|
238
|
+
self._checksquare()
|
|
239
|
+
n = self.shape()[0]
|
|
240
|
+
return sum(self._data[i][i] for i in range(n))
|
|
241
|
+
|
|
242
|
+
def rank(self) -> int:
|
|
243
|
+
"""
|
|
244
|
+
Compute the rank of the matrix using Gaussian elimination to row-echelon form,
|
|
245
|
+
counting pivots with epsilon tolerance.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The rank as an integer.
|
|
249
|
+
"""
|
|
250
|
+
a = self._copy()
|
|
251
|
+
m, n = self.shape()
|
|
252
|
+
r = 0
|
|
253
|
+
c = 0
|
|
254
|
+
|
|
255
|
+
while r < m and c < n:
|
|
256
|
+
# Find pivot row (partial pivoting for stability)
|
|
257
|
+
piv = self._pivot(a, r, c)
|
|
258
|
+
if piv is None:
|
|
259
|
+
c += 1
|
|
260
|
+
continue
|
|
261
|
+
if piv != r:
|
|
262
|
+
a[r], a[piv] = a[piv], a[r]
|
|
263
|
+
|
|
264
|
+
pivot = a[r][c]
|
|
265
|
+
# Eliminate below
|
|
266
|
+
for i in range(r + 1, m):
|
|
267
|
+
if abs(a[i][c]) <= self.epsilon:
|
|
268
|
+
continue
|
|
269
|
+
factor = a[i][c] / pivot
|
|
270
|
+
a[i][c] = 0.0
|
|
271
|
+
for j in range(c + 1, n):
|
|
272
|
+
a[i][j] -= factor * a[r][j]
|
|
273
|
+
|
|
274
|
+
r += 1
|
|
275
|
+
c += 1
|
|
276
|
+
|
|
277
|
+
# Count non-zero rows (in echelon form, pivots correspond to rank)
|
|
278
|
+
rank = 0
|
|
279
|
+
for i in range(m):
|
|
280
|
+
if any(abs(a[i][j]) > self.epsilon for j in range(n)):
|
|
281
|
+
rank += 1
|
|
282
|
+
return rank
|
|
283
|
+
|
|
284
|
+
# -------------------------
|
|
285
|
+
# Core linear algebra
|
|
286
|
+
# -------------------------
|
|
287
|
+
|
|
288
|
+
def det(self) -> float:
|
|
289
|
+
"""
|
|
290
|
+
Compute determinant det(A) for a square matrix using Gaussian elimination
|
|
291
|
+
with partial pivoting (more stable than Laplace expansion).
|
|
292
|
+
|
|
293
|
+
Algorithm idea:
|
|
294
|
+
- Convert A to upper-triangular U via elimination.
|
|
295
|
+
- det(A) = sign * product(diagonal(U)), where sign flips on each row swap.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Determinant as a float.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
ValueError: If the matrix is not square.
|
|
302
|
+
"""
|
|
303
|
+
self._checksquare()
|
|
304
|
+
a = self._copy()
|
|
305
|
+
n = self.shape()[0]
|
|
306
|
+
sign = 1.0
|
|
307
|
+
|
|
308
|
+
for c in range(n):
|
|
309
|
+
piv = self._pivot(a, c, c)
|
|
310
|
+
if piv is None:
|
|
311
|
+
return 0.0
|
|
312
|
+
if piv != c:
|
|
313
|
+
a[c], a[piv] = a[piv], a[c]
|
|
314
|
+
sign *= -1.0
|
|
315
|
+
|
|
316
|
+
pivot = a[c][c]
|
|
317
|
+
if abs(pivot) <= self.epsilon:
|
|
318
|
+
return 0.0
|
|
319
|
+
|
|
320
|
+
# Eliminate below pivot
|
|
321
|
+
for r in range(c + 1, n):
|
|
322
|
+
if abs(a[r][c]) <= self.epsilon:
|
|
323
|
+
continue
|
|
324
|
+
factor = a[r][c] / pivot
|
|
325
|
+
a[r][c] = 0.0
|
|
326
|
+
for j in range(c + 1, n):
|
|
327
|
+
a[r][j] -= factor * a[c][j]
|
|
328
|
+
|
|
329
|
+
detv = sign
|
|
330
|
+
for i in range(n):
|
|
331
|
+
detv *= a[i][i]
|
|
332
|
+
# Treat very small results as zero (tolerance)
|
|
333
|
+
return 0.0 if abs(detv) <= self.epsilon else detv
|
|
334
|
+
|
|
335
|
+
def minor(self, i: int, j: int) -> float:
|
|
336
|
+
"""
|
|
337
|
+
Compute the minor determinant after removing row i and column j.
|
|
338
|
+
|
|
339
|
+
Indices are 0-based.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
i: Row index to remove.
|
|
343
|
+
j: Column index to remove.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
det(M_ij) as float, where M_ij is the submatrix.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
ValueError: If matrix is not square or indices out of range.
|
|
350
|
+
"""
|
|
351
|
+
self._checksquare()
|
|
352
|
+
n = self.shape()[0]
|
|
353
|
+
if not (0 <= i < n and 0 <= j < n):
|
|
354
|
+
raise ValueError("minor indices out of range")
|
|
355
|
+
sub = []
|
|
356
|
+
for r in range(n):
|
|
357
|
+
if r == i:
|
|
358
|
+
continue
|
|
359
|
+
row = []
|
|
360
|
+
for c in range(n):
|
|
361
|
+
if c == j:
|
|
362
|
+
continue
|
|
363
|
+
row.append(self._data[r][c])
|
|
364
|
+
sub.append(row)
|
|
365
|
+
return Matrix(sub).det()
|
|
366
|
+
|
|
367
|
+
def cofactor(self, i: int, j: int) -> float:
|
|
368
|
+
"""
|
|
369
|
+
Compute the cofactor C_ij = (-1)^(i+j) * minor(i, j).
|
|
370
|
+
|
|
371
|
+
Indices are 0-based.
|
|
372
|
+
"""
|
|
373
|
+
s = -1.0 if (i + j) % 2 == 1 else 1.0
|
|
374
|
+
return s * self.minor(i, j)
|
|
375
|
+
|
|
376
|
+
def adjoint(self) -> "Matrix":
|
|
377
|
+
"""
|
|
378
|
+
Return the adjoint (adjugate) matrix: adj(A) = C^T,
|
|
379
|
+
where C is the cofactor matrix.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
ValueError: If matrix is not square.
|
|
383
|
+
"""
|
|
384
|
+
self._checksquare()
|
|
385
|
+
n = self.shape()[0]
|
|
386
|
+
cof = [[0.0] * n for _ in range(n)]
|
|
387
|
+
for i in range(n):
|
|
388
|
+
for j in range(n):
|
|
389
|
+
cof[i][j] = self.cofactor(i, j)
|
|
390
|
+
return Matrix(cof).transpose()
|
|
391
|
+
|
|
392
|
+
def inv(self) -> "Matrix":
|
|
393
|
+
"""
|
|
394
|
+
Compute the inverse A^{-1} using Gauss-Jordan elimination with partial pivoting.
|
|
395
|
+
|
|
396
|
+
Algorithm idea:
|
|
397
|
+
- Form augmented matrix [A | I]
|
|
398
|
+
- Row-reduce left side to I, applying the same operations to the right side.
|
|
399
|
+
- Right side becomes A^{-1}.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
The inverse matrix.
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If matrix is not square or is singular (non-invertible).
|
|
406
|
+
"""
|
|
407
|
+
self._checksquare()
|
|
408
|
+
n = self.shape()[0]
|
|
409
|
+
|
|
410
|
+
# Build augmented matrix [A | I]
|
|
411
|
+
aug = [self._data[i][:] + Matrix.eye(n)._data[i][:] for i in range(n)]
|
|
412
|
+
|
|
413
|
+
for c in range(n):
|
|
414
|
+
# Pivot selection (max abs value in column c)
|
|
415
|
+
piv = self._pivot(aug, c, c)
|
|
416
|
+
if piv is None:
|
|
417
|
+
raise ValueError("matrix is singular (pivot not found)")
|
|
418
|
+
if piv != c:
|
|
419
|
+
aug[c], aug[piv] = aug[piv], aug[c]
|
|
420
|
+
|
|
421
|
+
pivot = aug[c][c]
|
|
422
|
+
if abs(pivot) <= self.epsilon:
|
|
423
|
+
raise ValueError("matrix is singular (zero pivot)")
|
|
424
|
+
|
|
425
|
+
# Normalize pivot row
|
|
426
|
+
invp = 1.0 / pivot
|
|
427
|
+
for j in range(2 * n):
|
|
428
|
+
aug[c][j] *= invp
|
|
429
|
+
|
|
430
|
+
# Eliminate other rows
|
|
431
|
+
for r in range(n):
|
|
432
|
+
if r == c:
|
|
433
|
+
continue
|
|
434
|
+
factor = aug[r][c]
|
|
435
|
+
if abs(factor) <= self.epsilon:
|
|
436
|
+
aug[r][c] = 0.0
|
|
437
|
+
continue
|
|
438
|
+
aug[r][c] = 0.0
|
|
439
|
+
for j in range(c + 1, 2 * n):
|
|
440
|
+
aug[r][j] -= factor * aug[c][j]
|
|
441
|
+
|
|
442
|
+
invdata = [row[n:] for row in aug]
|
|
443
|
+
return Matrix(invdata)
|
|
444
|
+
|
|
445
|
+
# -------------------------
|
|
446
|
+
# Advanced
|
|
447
|
+
# -------------------------
|
|
448
|
+
|
|
449
|
+
def solve(self, b: Union[Sequence[Real], "Matrix"]) -> "Matrix":
|
|
450
|
+
"""
|
|
451
|
+
Solve the linear system Ax = b for x using Gaussian elimination with partial pivoting.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
b:
|
|
455
|
+
- A length-n sequence (treated as an n x 1 column vector), or
|
|
456
|
+
- A Matrix with shape (n, k) for multiple right-hand sides.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Solution matrix x with shape (n, 1) or (n, k).
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ValueError: If A is not square, dimensions mismatch, or the system is singular.
|
|
463
|
+
"""
|
|
464
|
+
self._checksquare()
|
|
465
|
+
n = self.shape()[0]
|
|
466
|
+
|
|
467
|
+
if isinstance(b, Matrix):
|
|
468
|
+
br, bc = b.shape()
|
|
469
|
+
if br != n:
|
|
470
|
+
raise ValueError("dimension mismatch: b must have the same row count as A")
|
|
471
|
+
rhs = b._copy()
|
|
472
|
+
k = bc
|
|
473
|
+
else:
|
|
474
|
+
if len(b) != n:
|
|
475
|
+
raise ValueError("dimension mismatch: b length must match A size")
|
|
476
|
+
rhs = [[float(x)] for x in b]
|
|
477
|
+
k = 1
|
|
478
|
+
|
|
479
|
+
a = self._copy()
|
|
480
|
+
|
|
481
|
+
# Forward elimination to upper-triangular form
|
|
482
|
+
for c in range(n):
|
|
483
|
+
piv = self._pivot(a, c, c)
|
|
484
|
+
if piv is None:
|
|
485
|
+
raise ValueError("system is singular (pivot not found)")
|
|
486
|
+
if piv != c:
|
|
487
|
+
a[c], a[piv] = a[piv], a[c]
|
|
488
|
+
rhs[c], rhs[piv] = rhs[piv], rhs[c]
|
|
489
|
+
|
|
490
|
+
pivot = a[c][c]
|
|
491
|
+
if abs(pivot) <= self.epsilon:
|
|
492
|
+
raise ValueError("system is singular (zero pivot)")
|
|
493
|
+
|
|
494
|
+
for r in range(c + 1, n):
|
|
495
|
+
if abs(a[r][c]) <= self.epsilon:
|
|
496
|
+
a[r][c] = 0.0
|
|
497
|
+
continue
|
|
498
|
+
factor = a[r][c] / pivot
|
|
499
|
+
a[r][c] = 0.0
|
|
500
|
+
for j in range(c + 1, n):
|
|
501
|
+
a[r][j] -= factor * a[c][j]
|
|
502
|
+
for t in range(k):
|
|
503
|
+
rhs[r][t] -= factor * rhs[c][t]
|
|
504
|
+
|
|
505
|
+
# Back substitution
|
|
506
|
+
x = [[0.0] * k for _ in range(n)]
|
|
507
|
+
for i in range(n - 1, -1, -1):
|
|
508
|
+
pivot = a[i][i]
|
|
509
|
+
if abs(pivot) <= self.epsilon:
|
|
510
|
+
raise ValueError("system is singular (zero pivot during back substitution)")
|
|
511
|
+
for t in range(k):
|
|
512
|
+
s = rhs[i][t]
|
|
513
|
+
for j in range(i + 1, n):
|
|
514
|
+
s -= a[i][j] * x[j][t]
|
|
515
|
+
x[i][t] = s / pivot
|
|
516
|
+
|
|
517
|
+
return Matrix(x)
|
|
518
|
+
|
|
519
|
+
def decompose(self) -> Tuple["Matrix", "Matrix"]:
|
|
520
|
+
"""
|
|
521
|
+
Compute LU decomposition (Doolittle) WITHOUT pivoting: A = L U.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
(L, U) where:
|
|
525
|
+
- L is lower triangular with 1s on the diagonal,
|
|
526
|
+
- U is upper triangular.
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
ValueError: If matrix is not square, or a zero/near-zero pivot is encountered.
|
|
530
|
+
"""
|
|
531
|
+
self._checksquare()
|
|
532
|
+
n = self.shape()[0]
|
|
533
|
+
a = self._copy()
|
|
534
|
+
|
|
535
|
+
L = Matrix.eye(n)._data
|
|
536
|
+
U = Matrix.zeros(n, n)._data
|
|
537
|
+
|
|
538
|
+
for i in range(n):
|
|
539
|
+
# Compute U[i][j] for j >= i
|
|
540
|
+
for j in range(i, n):
|
|
541
|
+
s = 0.0
|
|
542
|
+
for k in range(i):
|
|
543
|
+
s += L[i][k] * U[k][j]
|
|
544
|
+
U[i][j] = a[i][j] - s
|
|
545
|
+
|
|
546
|
+
pivot = U[i][i]
|
|
547
|
+
if abs(pivot) <= self.epsilon:
|
|
548
|
+
raise ValueError("LU decomposition failed (zero pivot); try a different method with pivoting")
|
|
549
|
+
|
|
550
|
+
# Compute L[j][i] for j > i
|
|
551
|
+
for j in range(i + 1, n):
|
|
552
|
+
s = 0.0
|
|
553
|
+
for k in range(i):
|
|
554
|
+
s += L[j][k] * U[k][i]
|
|
555
|
+
L[j][i] = (a[j][i] - s) / pivot
|
|
556
|
+
|
|
557
|
+
return Matrix(L), Matrix(U)
|
|
558
|
+
|
|
559
|
+
# -------------------------
|
|
560
|
+
# Internal helpers (private)
|
|
561
|
+
# -------------------------
|
|
562
|
+
|
|
563
|
+
def _checkshape(self, other: "Matrix") -> None:
|
|
564
|
+
if not isinstance(other, Matrix):
|
|
565
|
+
raise TypeError("other must be a Matrix")
|
|
566
|
+
if self.shape() != other.shape():
|
|
567
|
+
raise ValueError("shape mismatch")
|
|
568
|
+
|
|
569
|
+
def _checksquare(self) -> None:
|
|
570
|
+
m, n = self.shape()
|
|
571
|
+
if m != n:
|
|
572
|
+
raise ValueError("matrix must be square")
|
|
573
|
+
|
|
574
|
+
def _copy(self) -> List[List[float]]:
|
|
575
|
+
return [row[:] for row in self._data]
|
|
576
|
+
|
|
577
|
+
def _pivot(self, a: List[List[float]], start: int, col: int) -> Optional[int]:
|
|
578
|
+
"""
|
|
579
|
+
Return the row index of the best pivot in column 'col' at or below 'start',
|
|
580
|
+
using max-absolute-value selection. Return None if all candidates are ~0.
|
|
581
|
+
"""
|
|
582
|
+
best = None
|
|
583
|
+
bestv = 0.0
|
|
584
|
+
for r in range(start, len(a)):
|
|
585
|
+
v = abs(a[r][col])
|
|
586
|
+
if v > bestv:
|
|
587
|
+
bestv = v
|
|
588
|
+
best = r
|
|
589
|
+
if best is None or bestv <= self.epsilon:
|
|
590
|
+
return None
|
|
591
|
+
return best
|
|
592
|
+
|
|
593
|
+
def power_iteration(self, num_simulations: int = 100) -> Tuple[float, "Matrix"]:
|
|
594
|
+
"""
|
|
595
|
+
使用幂法 (Power Iteration) 求方阵的主特征值 (绝对值最大的特征值) 及其对应的特征向量。
|
|
596
|
+
这在工程实践 (如 PCA 获取第一主成分、搜索引擎的 PageRank 算法) 中是极其核心的底层算法。
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
num_simulations: 迭代次数。通常 100 次对于常规矩阵已足够收敛。
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
(eigenvalue, eigenvector): 一个元组,包含特征值 (float) 和 对应的特征向量 (n x 1 的 Matrix 对象)。
|
|
603
|
+
|
|
604
|
+
Raises:
|
|
605
|
+
ValueError: 如果矩阵不是方阵。
|
|
606
|
+
"""
|
|
607
|
+
self._checksquare()
|
|
608
|
+
n = self.shape()[0]
|
|
609
|
+
|
|
610
|
+
# 1. 初始猜测向量:为了兼容现有的库,我们生成一个 n x 1 的全 1 列向量矩阵
|
|
611
|
+
b_k = Matrix([[1.0] for _ in range(n)])
|
|
612
|
+
|
|
613
|
+
# 2. 核心迭代过程
|
|
614
|
+
for _ in range(num_simulations):
|
|
615
|
+
# 计算 A * b_k
|
|
616
|
+
b_k1 = self.dot(b_k)
|
|
617
|
+
|
|
618
|
+
# 计算 b_k1 的 2-范数 (即向量的欧几里得长度)
|
|
619
|
+
norm_sq = 0.0
|
|
620
|
+
for i in range(n):
|
|
621
|
+
norm_sq += b_k1._data[i][0] ** 2
|
|
622
|
+
norm = norm_sq ** 0.5
|
|
623
|
+
|
|
624
|
+
# 防止除以零 (极度奇异或全零矩阵)
|
|
625
|
+
if norm <= self.epsilon:
|
|
626
|
+
break
|
|
627
|
+
|
|
628
|
+
# 归一化:将向量长度缩放为 1
|
|
629
|
+
b_k1 = b_k1.scale(1.0 / norm)
|
|
630
|
+
b_k = b_k1
|
|
631
|
+
|
|
632
|
+
# 3. 计算瑞利商 (Rayleigh quotient) 提取特征值
|
|
633
|
+
# 特征值 = (b_k^T * A * b_k) / (b_k^T * b_k)
|
|
634
|
+
# 因为 b_k 已经是长度为 1 的单位向量,分母为 1,直接计算分子即可
|
|
635
|
+
Ab_k = self.dot(b_k)
|
|
636
|
+
eigenvalue = b_k.transpose().dot(Ab_k)._data[0][0]
|
|
637
|
+
|
|
638
|
+
return eigenvalue, b_k
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from pymatrix import Matrix
|
|
3
|
+
|
|
4
|
+
class TestBasic(unittest.TestCase):
|
|
5
|
+
def test_det(self):
|
|
6
|
+
A = Matrix([[1, 2], [3, 4]])
|
|
7
|
+
self.assertAlmostEqual(A.det(), -2.0, places=6)
|
|
8
|
+
|
|
9
|
+
def test_inv(self):
|
|
10
|
+
A = Matrix([[1, 2], [3, 4]])
|
|
11
|
+
invA = A.inv()
|
|
12
|
+
I = A @ invA
|
|
13
|
+
self.assertAlmostEqual(I._data[0][0], 1.0, places=5)
|
|
14
|
+
self.assertAlmostEqual(I._data[1][1], 1.0, places=5)
|
|
15
|
+
self.assertAlmostEqual(I._data[0][1], 0.0, places=5)
|
|
16
|
+
self.assertAlmostEqual(I._data[1][0], 0.0, places=5)
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
unittest.main()
|