lumpy-la 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.
- lumpy_la-0.1.0/PKG-INFO +6 -0
- lumpy_la-0.1.0/README.md +337 -0
- lumpy_la-0.1.0/lumpy/__init__.py +8 -0
- lumpy_la-0.1.0/lumpy/constructors.py +60 -0
- lumpy_la-0.1.0/lumpy/core.py +41 -0
- lumpy_la-0.1.0/lumpy/decompositions.py +17 -0
- lumpy_la-0.1.0/lumpy/equations.py +15 -0
- lumpy_la-0.1.0/lumpy/geometry.py +36 -0
- lumpy_la-0.1.0/lumpy/spaces.py +53 -0
- lumpy_la-0.1.0/lumpy/utils.py +5 -0
- lumpy_la-0.1.0/lumpy_la.egg-info/PKG-INFO +6 -0
- lumpy_la-0.1.0/lumpy_la.egg-info/SOURCES.txt +16 -0
- lumpy_la-0.1.0/lumpy_la.egg-info/dependency_links.txt +1 -0
- lumpy_la-0.1.0/lumpy_la.egg-info/requires.txt +1 -0
- lumpy_la-0.1.0/lumpy_la.egg-info/top_level.txt +1 -0
- lumpy_la-0.1.0/pyproject.toml +13 -0
- lumpy_la-0.1.0/setup.cfg +4 -0
- lumpy_la-0.1.0/tests/test_lumpy.py +523 -0
lumpy_la-0.1.0/PKG-INFO
ADDED
lumpy_la-0.1.0/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Lumpy
|
|
2
|
+
|
|
3
|
+
**Very elegant. Very Lumpy.**
|
|
4
|
+
|
|
5
|
+
Lumpy is a tiny semantic linear algebra wrapper over NumPy.
|
|
6
|
+
|
|
7
|
+
NumPy is excellent, but its core abstraction is the n-dimensional array, not the mathematical vector or matrix. Lumpy uses NumPy underneath while enforcing a more linear-algebraic convention:
|
|
8
|
+
|
|
9
|
+
- vectors are column vectors
|
|
10
|
+
- matrices are built from columns
|
|
11
|
+
- rows are transposes or row slices
|
|
12
|
+
- projections are onto column spaces
|
|
13
|
+
- subspace tools return bases as columns
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install lumpy-la
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import lumpy as la
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Vectors
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
v = la.vec(1, 2, 3)
|
|
31
|
+
|
|
32
|
+
v.shape
|
|
33
|
+
# (3, 1)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Vectors are always column vectors.
|
|
37
|
+
|
|
38
|
+
$$
|
|
39
|
+
\mathbf{v}\in \mathbb{R}^{n\times 1}
|
|
40
|
+
$$
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
v.T
|
|
44
|
+
# array([[1., 2., 3.]])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
$$
|
|
48
|
+
\mathbf{v}^{T}\in \mathbb{R}^{1\times n}
|
|
49
|
+
$$
|
|
50
|
+
|
|
51
|
+
## Matrices are built from columns
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
A = la.mat(
|
|
55
|
+
[1, 2, 3],
|
|
56
|
+
[4, 5, 6]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
A
|
|
60
|
+
# array([[1., 4.],
|
|
61
|
+
# [2., 5.],
|
|
62
|
+
# [3., 6.]])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also pass existing column vectors:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
u = la.vec(1, 2, 3)
|
|
69
|
+
v = la.vec(4, 5, 6)
|
|
70
|
+
|
|
71
|
+
A = la.mat(u, v)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Matrices from rows
|
|
75
|
+
|
|
76
|
+
`mat()` constructs matrices from columns. Sometimes, though, it is useful to type a matrix in the same visual layout used on paper.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
A = la.matt(
|
|
80
|
+
[1, 2, 3],
|
|
81
|
+
[4, 5, 6]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
A
|
|
85
|
+
# array([[1., 2., 3.],
|
|
86
|
+
# [4., 5., 6.]])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```matt()``` preserves the usual visual layout of matrices, while ```mat()``` preserves Lumpy’s column-oriented semantics.
|
|
90
|
+
|
|
91
|
+
## Standard basis vectors
|
|
92
|
+
|
|
93
|
+
Lumpy uses Python-style zero-indexing.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
la.e(3, 0)
|
|
97
|
+
# array([[1.],
|
|
98
|
+
# [0.],
|
|
99
|
+
# [0.]])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
So ```la.e(n, i)``` corresponds to the mathematical basis vector $\mathbf{e}_{i+1}\in \mathbb{R}^{n}$ .
|
|
103
|
+
|
|
104
|
+
## Basic operations
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
u = la.vec(1, 2, 3)
|
|
108
|
+
v = la.vec(4, 5, 6)
|
|
109
|
+
|
|
110
|
+
la.inner(u, v)
|
|
111
|
+
# array([[32.]])
|
|
112
|
+
|
|
113
|
+
la.dot(u, v)
|
|
114
|
+
# 32.0
|
|
115
|
+
|
|
116
|
+
la.norm(v)
|
|
117
|
+
# 8.774964387392123
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```inner``` preserves matrix structure.
|
|
121
|
+
|
|
122
|
+
$$
|
|
123
|
+
\langle \mathbf{u},\mathbf{v} \rangle = [\mathbf{u}\cdot \mathbf{v}]\in \mathbb{R}^{1\times 1}
|
|
124
|
+
$$
|
|
125
|
+
|
|
126
|
+
```dot``` returns a scalar.
|
|
127
|
+
|
|
128
|
+
$$
|
|
129
|
+
\mathbf{u} \cdot \mathbf{v} \in \mathbb{R}
|
|
130
|
+
$$
|
|
131
|
+
|
|
132
|
+
## Unit vectors and normalization
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
v = la.vec(3, 4)
|
|
136
|
+
|
|
137
|
+
la.unit(v)
|
|
138
|
+
# array([[0.6],
|
|
139
|
+
# [0.8]])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For matrices, ```normalize``` normalizes each column:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
A = la.mat(
|
|
146
|
+
[3, 4],
|
|
147
|
+
[5, 12]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
la.normalize(A)
|
|
151
|
+
# array([[0.6 , 0.38461538],
|
|
152
|
+
# [0.8 , 0.92307692]])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Columns and rows
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
A = la.mat(
|
|
159
|
+
[1, 2, 3],
|
|
160
|
+
[4, 5, 6]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
la.col(A, 0)
|
|
164
|
+
# array([[1.],
|
|
165
|
+
# [2.],
|
|
166
|
+
# [3.]])
|
|
167
|
+
|
|
168
|
+
la.row(A, 1)
|
|
169
|
+
# array([[2., 5.]])
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```col``` preserves column-vector shape.
|
|
173
|
+
|
|
174
|
+
## Projection
|
|
175
|
+
|
|
176
|
+
Project onto the column space of a matrix:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
A = la.mat(
|
|
180
|
+
[1, 0, 0],
|
|
181
|
+
[0, 1, 0]
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
v = la.vec(3, 4, 5)
|
|
185
|
+
|
|
186
|
+
la.proj(A, v)
|
|
187
|
+
# array([[3.],
|
|
188
|
+
# [4.],
|
|
189
|
+
# [0.]])
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This projects $\mathbf{v}$ onto the $xy$-plane.
|
|
193
|
+
|
|
194
|
+
Vector projection is just the rank-one case:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
u = la.vec(1, 1, 0)
|
|
198
|
+
v = la.vec(2, 3, 4)
|
|
199
|
+
|
|
200
|
+
la.proj(u, v)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Subspaces
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
A = la.mat(
|
|
207
|
+
[1, 2],
|
|
208
|
+
[2, 4]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
la.rank(A)
|
|
212
|
+
# 1
|
|
213
|
+
|
|
214
|
+
la.independent(A)
|
|
215
|
+
# False
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
An orthonormal basis for the column space:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
Q = la.orth(A)
|
|
222
|
+
|
|
223
|
+
Q.T @ Q
|
|
224
|
+
# approximately identity
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Nullspace basis:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
N = la.null(A)
|
|
231
|
+
|
|
232
|
+
A @ N
|
|
233
|
+
# approximately zero
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Lumpy returns subspace bases as columns.
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
|
|
240
|
+
A = la.matt(
|
|
241
|
+
[1, 2, 3],
|
|
242
|
+
[2, 4, 6]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
la.row_space(A)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The left nullspace is the nullspace of $A^{T}$:
|
|
249
|
+
|
|
250
|
+
$$
|
|
251
|
+
\mathrm{Null}\left(A^{T}\right)
|
|
252
|
+
$$
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
la.left_null(A)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## SVD
|
|
259
|
+
|
|
260
|
+
By default, Lumpy's SVD returns only rank-relevant singular directions:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
U, s, Vt = la.svd(A)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For full ambient bases:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
U, s, Vt = la.svd(A, full_matrices=True)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Many Lumpy subspace functions use a numerical tolerance, defaulting to `tol=1e-12`, to decide which singular values count as nonzero.
|
|
273
|
+
|
|
274
|
+
## Geometry
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
u = la.vec(1, 0)
|
|
278
|
+
v = la.vec(0, 1)
|
|
279
|
+
|
|
280
|
+
la.angle(u, v)
|
|
281
|
+
# 1.5707963267948966
|
|
282
|
+
|
|
283
|
+
la.dist(u, v)
|
|
284
|
+
# 1.4142135623730951
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Solving systems
|
|
288
|
+
|
|
289
|
+
Prefer `solve()` over explicitly computing an inverse.
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
|
|
293
|
+
A = la.matt(
|
|
294
|
+
[2, 0],
|
|
295
|
+
[0, 3]
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
b = la.vec(4, 9)
|
|
299
|
+
la.solve(A, b)
|
|
300
|
+
|
|
301
|
+
# array([[2.],
|
|
302
|
+
# [3.]])
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
For overdetermined systems, use least squares:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
A = la.matt(
|
|
309
|
+
[1, 1],
|
|
310
|
+
[1, 2],
|
|
311
|
+
[1, 3]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
b = la.vec(1, 2, 2)
|
|
315
|
+
|
|
316
|
+
la.lstsq(A, b)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Running tests
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
python -m pytest
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Design principle
|
|
326
|
+
|
|
327
|
+
Lumpy is not trying to replace NumPy.
|
|
328
|
+
|
|
329
|
+
It is a small semantic layer for doing linear algebra with conventions that feel closer to the math.
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
import numpy as np
|
|
333
|
+
import lumpy as la
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Use NumPy for general arrays.
|
|
337
|
+
Use Lumpy when you want vectors, matrices, projections, and subspaces to behave like linear algebra objects.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .utils import is_vector
|
|
3
|
+
|
|
4
|
+
def mat(*columns, dtype=float):
|
|
5
|
+
""""
|
|
6
|
+
Construct a matrix from columns.
|
|
7
|
+
|
|
8
|
+
Lists are interpreted as column entries. Existing column
|
|
9
|
+
vectors may also be passed directly.
|
|
10
|
+
"""
|
|
11
|
+
processed = []
|
|
12
|
+
|
|
13
|
+
for col in columns:
|
|
14
|
+
# Python list -> column vector
|
|
15
|
+
if isinstance(col, list):
|
|
16
|
+
col = np.array(col, dtype=dtype).reshape(-1,1)
|
|
17
|
+
|
|
18
|
+
if col.ndim != 2 or col.shape[1] != 1:
|
|
19
|
+
raise ValueError("mat() expects columns: lists or column vectors.")
|
|
20
|
+
|
|
21
|
+
processed.append(col)
|
|
22
|
+
|
|
23
|
+
if not processed:
|
|
24
|
+
raise ValueError("mat() requires at least one column.")
|
|
25
|
+
|
|
26
|
+
return np.hstack(processed).astype(dtype)
|
|
27
|
+
|
|
28
|
+
def matt(*rows, dtype=float):
|
|
29
|
+
"""
|
|
30
|
+
Construct a matrix from rows.
|
|
31
|
+
|
|
32
|
+
matt() preserves the usual visual layout of matrices,
|
|
33
|
+
while mat() preserves Lumpy's column-oriented semantics.
|
|
34
|
+
"""
|
|
35
|
+
if not rows:
|
|
36
|
+
raise ValueError("matt() requires at least one row.")
|
|
37
|
+
return mat(*rows, dtype=dtype).T
|
|
38
|
+
|
|
39
|
+
def vec(*entries, dtype=float):
|
|
40
|
+
"""Construct a column vector."""
|
|
41
|
+
return mat(list(entries), dtype=dtype)
|
|
42
|
+
|
|
43
|
+
def eye(n, dtype=float):
|
|
44
|
+
"""Construct the n x n identity matrix."""
|
|
45
|
+
return np.eye(n, dtype=dtype)
|
|
46
|
+
|
|
47
|
+
def e(n, i, dtype=float):
|
|
48
|
+
"""
|
|
49
|
+
e(n, i) returns the standard basis vector corresponding
|
|
50
|
+
to the mathematical vector e_(i+1) in R^n.
|
|
51
|
+
|
|
52
|
+
Indexing is 0-based to remain consistent with Python/NumPy.
|
|
53
|
+
"""
|
|
54
|
+
e = np.zeros((n,1), dtype=dtype)
|
|
55
|
+
e[i] = 1
|
|
56
|
+
return e
|
|
57
|
+
|
|
58
|
+
def diag(*entries, dtype=float):
|
|
59
|
+
"""Construct a diagonal matrix with given entries."""
|
|
60
|
+
return np.diag(entries).astype(dtype)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .utils import is_vector
|
|
3
|
+
|
|
4
|
+
def col(A, i):
|
|
5
|
+
"""Return column i as a column vector."""
|
|
6
|
+
return A[:, [i]]
|
|
7
|
+
|
|
8
|
+
def row(A, i):
|
|
9
|
+
"""Return row i as a row matrix."""
|
|
10
|
+
return A[[i], :]
|
|
11
|
+
|
|
12
|
+
def inner(A,B):
|
|
13
|
+
"""Inner product of matrices."""
|
|
14
|
+
return A.T @ B
|
|
15
|
+
|
|
16
|
+
def dot(u,v):
|
|
17
|
+
"""Dot product of vectors."""
|
|
18
|
+
if not(is_vector(u) and is_vector(v)):
|
|
19
|
+
raise ValueError("dot() requires column vectors.")
|
|
20
|
+
return (u.T @ v).item()
|
|
21
|
+
|
|
22
|
+
def norm(v):
|
|
23
|
+
"""Euclidean norm of a column vector."""
|
|
24
|
+
return np.linalg.norm(v)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def T(A):
|
|
28
|
+
"""Transpose of A."""
|
|
29
|
+
return A.T
|
|
30
|
+
|
|
31
|
+
def tr(A):
|
|
32
|
+
"""Trace of A."""
|
|
33
|
+
return np.trace(A)
|
|
34
|
+
|
|
35
|
+
def adj(A):
|
|
36
|
+
"""Adjoint (conjugate transpose) of A."""
|
|
37
|
+
return A.conj().T
|
|
38
|
+
|
|
39
|
+
def det(A):
|
|
40
|
+
"""Determinant of A."""
|
|
41
|
+
return np.linalg.det(A)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def svd(A, full_matrices=False, tol=1e-12):
|
|
4
|
+
"""
|
|
5
|
+
Return the singular value decomposition of A.
|
|
6
|
+
|
|
7
|
+
By default, returns only the rank-relevant singular directions:
|
|
8
|
+
|
|
9
|
+
A = U @ diag(s) @ Vt
|
|
10
|
+
|
|
11
|
+
If full_matrices=True, returns NumPy's full SVD.
|
|
12
|
+
"""
|
|
13
|
+
U, s, Vt = np.linalg.svd(A, full_matrices=full_matrices)
|
|
14
|
+
if not full_matrices:
|
|
15
|
+
rank = np.sum(s > tol)
|
|
16
|
+
return U[:,:rank], s[:rank], Vt[:rank,:]
|
|
17
|
+
return U, s, Vt
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def solve(A, b):
|
|
4
|
+
"""
|
|
5
|
+
Solve Ax = b.
|
|
6
|
+
Prefer solve() over explicitly computing inv(A) @ b.
|
|
7
|
+
"""
|
|
8
|
+
return np.linalg.solve(A, b)
|
|
9
|
+
|
|
10
|
+
def lstsq(A, b, rcond=None):
|
|
11
|
+
"""
|
|
12
|
+
Return the least-squares solution to Ax ≈ b.
|
|
13
|
+
"""
|
|
14
|
+
x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=rcond)
|
|
15
|
+
return x
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .utils import is_vector
|
|
3
|
+
from .core import norm, dot
|
|
4
|
+
|
|
5
|
+
def dist(u, v):
|
|
6
|
+
"""
|
|
7
|
+
Return the Euclidean distance between two column vectors u and v.
|
|
8
|
+
"""
|
|
9
|
+
if not(is_vector(u) and is_vector(v)):
|
|
10
|
+
raise ValueError("dist() requires column vectors.")
|
|
11
|
+
return norm(u - v)
|
|
12
|
+
|
|
13
|
+
def angle(u, v):
|
|
14
|
+
"""
|
|
15
|
+
Return the angle in radians between two column vectors u and v.
|
|
16
|
+
"""
|
|
17
|
+
if not(is_vector(u) and is_vector(v)):
|
|
18
|
+
raise ValueError("angle() requires column vectors.")
|
|
19
|
+
return np.arccos(dot(u,v)/(norm(u)*norm(v)))
|
|
20
|
+
|
|
21
|
+
def normalize(A):
|
|
22
|
+
"""
|
|
23
|
+
Normalize each column of A.
|
|
24
|
+
"""
|
|
25
|
+
norms = np.linalg.norm(A, axis=0)
|
|
26
|
+
|
|
27
|
+
if np.any(norms == 0):
|
|
28
|
+
raise ValueError("Cannot normalize a matrix with zero columns.")
|
|
29
|
+
|
|
30
|
+
return A / norms
|
|
31
|
+
|
|
32
|
+
def unit(v):
|
|
33
|
+
"""Return unit vector in direction of v."""
|
|
34
|
+
if not is_vector(v):
|
|
35
|
+
raise ValueError("unit() requires a column vector.")
|
|
36
|
+
return normalize(v)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .decompositions import svd
|
|
3
|
+
|
|
4
|
+
def rank(A, tol=1e-12):
|
|
5
|
+
"""
|
|
6
|
+
Return the rank of A.
|
|
7
|
+
"""
|
|
8
|
+
_, s, _ = svd(A,tol=tol)
|
|
9
|
+
return len(s)
|
|
10
|
+
|
|
11
|
+
def orth(A,tol=1e-12):
|
|
12
|
+
"""
|
|
13
|
+
Return an orthonormal basis for the column space of A.
|
|
14
|
+
"""
|
|
15
|
+
U, _, _ = svd(A, tol=tol)
|
|
16
|
+
return U
|
|
17
|
+
|
|
18
|
+
def null(A, tol=1e-12):
|
|
19
|
+
"""
|
|
20
|
+
Return an orthonormal basis for the null space of A.
|
|
21
|
+
"""
|
|
22
|
+
U, s, Vt = svd(A, full_matrices=True, tol=tol)
|
|
23
|
+
rank = np.sum(s > tol)
|
|
24
|
+
return Vt[rank:].T
|
|
25
|
+
|
|
26
|
+
def proj(A,B,tol=1e-12):
|
|
27
|
+
"""
|
|
28
|
+
Orthogonal projection of B onto the column space of A.
|
|
29
|
+
Works for vectors and matrices.
|
|
30
|
+
"""
|
|
31
|
+
Q = orth(A,tol=tol)
|
|
32
|
+
return Q @ Q.T @ B
|
|
33
|
+
|
|
34
|
+
def independent(A):
|
|
35
|
+
"""
|
|
36
|
+
Return True if the columns of A are linearly independent.
|
|
37
|
+
"""
|
|
38
|
+
return rank(A) == A.shape[1]
|
|
39
|
+
|
|
40
|
+
def row_space(A, tol=1e-12):
|
|
41
|
+
"""
|
|
42
|
+
Return an orthonormal basis for the row space of A as columns.
|
|
43
|
+
"""
|
|
44
|
+
return orth(A.T, tol=tol)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def left_null(A, tol=1e-12):
|
|
48
|
+
"""
|
|
49
|
+
Return an orthonormal basis for the left nullspace of A as columns.
|
|
50
|
+
|
|
51
|
+
This is Null(A.T).
|
|
52
|
+
"""
|
|
53
|
+
return null(A.T, tol=tol)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
lumpy/__init__.py
|
|
4
|
+
lumpy/constructors.py
|
|
5
|
+
lumpy/core.py
|
|
6
|
+
lumpy/decompositions.py
|
|
7
|
+
lumpy/equations.py
|
|
8
|
+
lumpy/geometry.py
|
|
9
|
+
lumpy/spaces.py
|
|
10
|
+
lumpy/utils.py
|
|
11
|
+
lumpy_la.egg-info/PKG-INFO
|
|
12
|
+
lumpy_la.egg-info/SOURCES.txt
|
|
13
|
+
lumpy_la.egg-info/dependency_links.txt
|
|
14
|
+
lumpy_la.egg-info/requires.txt
|
|
15
|
+
lumpy_la.egg-info/top_level.txt
|
|
16
|
+
tests/test_lumpy.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lumpy
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lumpy-la"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Very elegant. Very Lumpy."
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = ["numpy"]
|
|
11
|
+
|
|
12
|
+
[tool.setuptools]
|
|
13
|
+
packages = ["lumpy"]
|
lumpy_la-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import lumpy as la
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def assert_close(A, B):
|
|
6
|
+
assert np.allclose(A, B)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# =========================
|
|
10
|
+
# Constructors
|
|
11
|
+
# =========================
|
|
12
|
+
|
|
13
|
+
def test_vec_is_column_vector():
|
|
14
|
+
v = la.vec(1, 2, 3)
|
|
15
|
+
|
|
16
|
+
assert v.shape == (3, 1)
|
|
17
|
+
|
|
18
|
+
expected = np.array([
|
|
19
|
+
[1],
|
|
20
|
+
[2],
|
|
21
|
+
[3]
|
|
22
|
+
], dtype=float)
|
|
23
|
+
|
|
24
|
+
assert_close(v, expected)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_mat_from_lists():
|
|
28
|
+
A = la.mat(
|
|
29
|
+
[1, 2, 3],
|
|
30
|
+
[4, 5, 6]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
expected = np.array([
|
|
34
|
+
[1, 4],
|
|
35
|
+
[2, 5],
|
|
36
|
+
[3, 6]
|
|
37
|
+
], dtype=float)
|
|
38
|
+
|
|
39
|
+
assert A.shape == (3, 2)
|
|
40
|
+
|
|
41
|
+
assert_close(A, expected)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_mat_mixed_inputs():
|
|
45
|
+
v = la.vec(1, 2, 3)
|
|
46
|
+
|
|
47
|
+
A = la.mat(
|
|
48
|
+
v,
|
|
49
|
+
[4, 5, 6]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expected = np.array([
|
|
53
|
+
[1, 4],
|
|
54
|
+
[2, 5],
|
|
55
|
+
[3, 6]
|
|
56
|
+
], dtype=float)
|
|
57
|
+
|
|
58
|
+
assert_close(A, expected)
|
|
59
|
+
|
|
60
|
+
def test_matt_from_rows():
|
|
61
|
+
A = la.matt(
|
|
62
|
+
[1, 2, 3],
|
|
63
|
+
[4, 5, 6]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expected = np.array([
|
|
67
|
+
[1, 2, 3],
|
|
68
|
+
[4, 5, 6]
|
|
69
|
+
], dtype=float)
|
|
70
|
+
|
|
71
|
+
assert_close(A, expected)
|
|
72
|
+
|
|
73
|
+
def test_mat_rejects_mismatched_columns():
|
|
74
|
+
try:
|
|
75
|
+
la.mat([1, 2, 3], [4, 5])
|
|
76
|
+
|
|
77
|
+
except ValueError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
raise AssertionError("mat() should reject mismatched column lengths")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_mat_rejects_empty_input():
|
|
85
|
+
try:
|
|
86
|
+
la.mat()
|
|
87
|
+
|
|
88
|
+
except ValueError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
raise AssertionError("mat() should require at least one column")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_eye():
|
|
96
|
+
assert_close(
|
|
97
|
+
la.eye(3),
|
|
98
|
+
np.eye(3)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_diag():
|
|
103
|
+
expected = np.array([
|
|
104
|
+
[1, 0, 0],
|
|
105
|
+
[0, 2, 0],
|
|
106
|
+
[0, 0, 3]
|
|
107
|
+
], dtype=float)
|
|
108
|
+
|
|
109
|
+
assert_close(
|
|
110
|
+
la.diag(1, 2, 3),
|
|
111
|
+
expected
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_e():
|
|
116
|
+
expected = np.array([
|
|
117
|
+
[0],
|
|
118
|
+
[1],
|
|
119
|
+
[0]
|
|
120
|
+
], dtype=float)
|
|
121
|
+
|
|
122
|
+
assert_close(
|
|
123
|
+
la.e(3, 1),
|
|
124
|
+
expected
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# =========================
|
|
129
|
+
# Core
|
|
130
|
+
# =========================
|
|
131
|
+
|
|
132
|
+
def test_col():
|
|
133
|
+
A = la.mat(
|
|
134
|
+
[1, 2, 3],
|
|
135
|
+
[4, 5, 6]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
c = la.col(A, 1)
|
|
139
|
+
|
|
140
|
+
assert c.shape == (3, 1)
|
|
141
|
+
|
|
142
|
+
assert_close(
|
|
143
|
+
c,
|
|
144
|
+
la.vec(4, 5, 6)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_row():
|
|
149
|
+
A = la.mat(
|
|
150
|
+
[1, 2, 3],
|
|
151
|
+
[4, 5, 6]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
r = la.row(A, 1)
|
|
155
|
+
|
|
156
|
+
expected = np.array([
|
|
157
|
+
[2, 5]
|
|
158
|
+
], dtype=float)
|
|
159
|
+
|
|
160
|
+
assert r.shape == (1, 2)
|
|
161
|
+
|
|
162
|
+
assert_close(r, expected)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_inner():
|
|
166
|
+
u = la.vec(1, 2, 3)
|
|
167
|
+
v = la.vec(4, 5, 6)
|
|
168
|
+
|
|
169
|
+
result = la.inner(u, v)
|
|
170
|
+
|
|
171
|
+
assert result.shape == (1, 1)
|
|
172
|
+
|
|
173
|
+
assert_close(
|
|
174
|
+
result,
|
|
175
|
+
np.array([[32.0]])
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_dot():
|
|
180
|
+
u = la.vec(1, 2, 3)
|
|
181
|
+
v = la.vec(4, 5, 6)
|
|
182
|
+
|
|
183
|
+
result = la.dot(u, v)
|
|
184
|
+
|
|
185
|
+
assert isinstance(result, float)
|
|
186
|
+
|
|
187
|
+
assert result == 32.0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_norm():
|
|
191
|
+
v = la.vec(3, 4)
|
|
192
|
+
|
|
193
|
+
assert la.norm(v) == 5.0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_unit():
|
|
197
|
+
v = la.vec(3, 4)
|
|
198
|
+
|
|
199
|
+
result = la.unit(v)
|
|
200
|
+
|
|
201
|
+
expected = la.vec(
|
|
202
|
+
3 / 5,
|
|
203
|
+
4 / 5
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
assert_close(result, expected)
|
|
207
|
+
|
|
208
|
+
assert np.isclose(
|
|
209
|
+
la.norm(result),
|
|
210
|
+
1.0
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_unit_rejects_matrix():
|
|
215
|
+
A = la.eye(2)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
la.unit(A)
|
|
219
|
+
|
|
220
|
+
except ValueError:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
else:
|
|
224
|
+
raise AssertionError(
|
|
225
|
+
"unit() should reject matrices"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_tr():
|
|
230
|
+
A = la.mat(
|
|
231
|
+
[1, 3],
|
|
232
|
+
[2, 4]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert la.tr(A) == 5
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_det():
|
|
239
|
+
A = la.mat(
|
|
240
|
+
[1, 3],
|
|
241
|
+
[2, 4]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
assert np.isclose(
|
|
245
|
+
la.det(A),
|
|
246
|
+
-2.0
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_adj():
|
|
251
|
+
A = la.mat(
|
|
252
|
+
[1, 2],
|
|
253
|
+
[3, 4]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
assert_close(
|
|
257
|
+
la.adj(A),
|
|
258
|
+
A.T
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# =========================
|
|
263
|
+
# Spaces
|
|
264
|
+
# =========================
|
|
265
|
+
|
|
266
|
+
def test_rank_full_rank():
|
|
267
|
+
A = la.eye(3)
|
|
268
|
+
|
|
269
|
+
assert la.rank(A) == 3
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_rank_deficient():
|
|
273
|
+
A = la.mat(
|
|
274
|
+
[1, 2, 3],
|
|
275
|
+
[2, 4, 6]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
assert la.rank(A) == 1
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_independent_true():
|
|
282
|
+
A = la.eye(3)
|
|
283
|
+
|
|
284
|
+
assert la.independent(A)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_independent_false():
|
|
288
|
+
A = la.mat(
|
|
289
|
+
[1, 2],
|
|
290
|
+
[2, 4]
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
assert not la.independent(A)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_orth():
|
|
297
|
+
A = la.mat(
|
|
298
|
+
[1, 0],
|
|
299
|
+
[2, 0]
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
Q = la.orth(A)
|
|
303
|
+
|
|
304
|
+
assert Q.shape == (2, 1)
|
|
305
|
+
|
|
306
|
+
assert_close(
|
|
307
|
+
Q.T @ Q,
|
|
308
|
+
np.array([[1.0]])
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_proj_vector():
|
|
313
|
+
A = la.mat([1, 0])
|
|
314
|
+
|
|
315
|
+
v = la.vec(3, 4)
|
|
316
|
+
|
|
317
|
+
result = la.proj(A, v)
|
|
318
|
+
|
|
319
|
+
expected = la.vec(3, 0)
|
|
320
|
+
|
|
321
|
+
assert_close(result, expected)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_proj_matrix():
|
|
325
|
+
A = la.mat(
|
|
326
|
+
[1, 0, 0],
|
|
327
|
+
[0, 1, 0]
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
v = la.vec(3, 4, 5)
|
|
331
|
+
|
|
332
|
+
result = la.proj(A, v)
|
|
333
|
+
|
|
334
|
+
expected = la.vec(3, 4, 0)
|
|
335
|
+
|
|
336
|
+
assert_close(result, expected)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_null():
|
|
340
|
+
A = la.mat(
|
|
341
|
+
[1, 0],
|
|
342
|
+
[0, 0]
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
N = la.null(A)
|
|
346
|
+
|
|
347
|
+
assert N.shape == (2, 1)
|
|
348
|
+
|
|
349
|
+
assert_close(
|
|
350
|
+
A @ N,
|
|
351
|
+
np.zeros((2, 1))
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
assert_close(
|
|
355
|
+
N.T @ N,
|
|
356
|
+
np.array([[1.0]])
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def test_row_space():
|
|
360
|
+
A = la.matt(
|
|
361
|
+
[1, 2, 3],
|
|
362
|
+
[2, 4, 6]
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
R = la.row_space(A)
|
|
366
|
+
|
|
367
|
+
assert R.shape == (3, 1)
|
|
368
|
+
assert_close(R.T @ R, np.array([[1.0]]))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_left_null():
|
|
372
|
+
A = la.matt(
|
|
373
|
+
[1, 2],
|
|
374
|
+
[2, 4],
|
|
375
|
+
[3, 6]
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
L = la.left_null(A)
|
|
379
|
+
|
|
380
|
+
assert_close(A.T @ L, np.zeros((2, L.shape[1])))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# =========================
|
|
384
|
+
# Geometry
|
|
385
|
+
# =========================
|
|
386
|
+
|
|
387
|
+
def test_normalize():
|
|
388
|
+
A = la.mat(
|
|
389
|
+
[3, 4],
|
|
390
|
+
[5, 12]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
N = la.normalize(A)
|
|
394
|
+
|
|
395
|
+
assert np.isclose(
|
|
396
|
+
la.norm(la.col(N, 0)),
|
|
397
|
+
1.0
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
assert np.isclose(
|
|
401
|
+
la.norm(la.col(N, 1)),
|
|
402
|
+
1.0
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
assert_close(
|
|
406
|
+
la.proj(
|
|
407
|
+
la.col(A, 0),
|
|
408
|
+
la.col(N, 0)
|
|
409
|
+
),
|
|
410
|
+
la.col(N, 0)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
assert_close(
|
|
414
|
+
la.proj(
|
|
415
|
+
la.col(A, 1),
|
|
416
|
+
la.col(N, 1)
|
|
417
|
+
),
|
|
418
|
+
la.col(N, 1)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_normalize_rejects_zero_column():
|
|
423
|
+
A = la.mat(
|
|
424
|
+
[1, 2],
|
|
425
|
+
[0, 0]
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
la.normalize(A)
|
|
430
|
+
|
|
431
|
+
except ValueError:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
else:
|
|
435
|
+
raise AssertionError(
|
|
436
|
+
"normalize() should reject zero columns"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def test_dist():
|
|
441
|
+
u = la.vec(1, 2)
|
|
442
|
+
v = la.vec(4, 6)
|
|
443
|
+
|
|
444
|
+
assert la.dist(u, v) == 5.0
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def test_angle():
|
|
448
|
+
u = la.vec(1, 0)
|
|
449
|
+
v = la.vec(0, 1)
|
|
450
|
+
|
|
451
|
+
assert np.isclose(
|
|
452
|
+
la.angle(u, v),
|
|
453
|
+
np.pi / 2
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# =========================
|
|
458
|
+
# Decompositions
|
|
459
|
+
# =========================
|
|
460
|
+
|
|
461
|
+
def test_svd_rank_trimmed():
|
|
462
|
+
A = la.mat(
|
|
463
|
+
[1, 0],
|
|
464
|
+
[2, 0]
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
U, s, Vt = la.svd(A)
|
|
468
|
+
|
|
469
|
+
assert len(s) == 1
|
|
470
|
+
|
|
471
|
+
assert U.shape == (2, 1)
|
|
472
|
+
|
|
473
|
+
assert Vt.shape == (1, 2)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_svd_full_matrices():
|
|
477
|
+
A = la.mat(
|
|
478
|
+
[1, 0],
|
|
479
|
+
[2, 0]
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
U, s, Vt = la.svd(
|
|
483
|
+
A,
|
|
484
|
+
full_matrices=True
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
assert U.shape == (2, 2)
|
|
488
|
+
|
|
489
|
+
assert Vt.shape == (2, 2)
|
|
490
|
+
|
|
491
|
+
# =========================
|
|
492
|
+
# Equations
|
|
493
|
+
# =========================
|
|
494
|
+
|
|
495
|
+
def test_solve():
|
|
496
|
+
A = la.matt(
|
|
497
|
+
[2, 0],
|
|
498
|
+
[0, 3]
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
b = la.vec(4, 9)
|
|
502
|
+
|
|
503
|
+
x = la.solve(A, b)
|
|
504
|
+
|
|
505
|
+
assert_close(x, la.vec(2, 3))
|
|
506
|
+
|
|
507
|
+
def test_lstsq():
|
|
508
|
+
A = la.matt(
|
|
509
|
+
[1, 1],
|
|
510
|
+
[1, 2],
|
|
511
|
+
[1, 3]
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
b = la.vec(1, 2, 2)
|
|
515
|
+
|
|
516
|
+
x = la.lstsq(A, b)
|
|
517
|
+
|
|
518
|
+
assert x.shape == (2, 1)
|
|
519
|
+
|
|
520
|
+
# Least-squares residual should be orthogonal to Col(A)
|
|
521
|
+
r = b - A @ x
|
|
522
|
+
|
|
523
|
+
assert_close(A.T @ r, np.zeros((2, 1)))
|