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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumpy-la
3
+ Version: 0.1.0
4
+ Summary: Very elegant. Very Lumpy.
5
+ Requires-Python: >=3.14
6
+ Requires-Dist: numpy
@@ -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,8 @@
1
+ from .constructors import *
2
+ from .core import *
3
+ from .spaces import *
4
+ from .geometry import *
5
+ from .utils import *
6
+ from .equations import *
7
+
8
+ from numpy.linalg import qr, eig, inv
@@ -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,5 @@
1
+ import numpy as np
2
+
3
+ def is_vector(v):
4
+ """Check if v is a column vector."""
5
+ return v.ndim == 2 and v.shape[1] == 1
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumpy-la
3
+ Version: 0.1.0
4
+ Summary: Very elegant. Very Lumpy.
5
+ Requires-Python: >=3.14
6
+ Requires-Dist: numpy
@@ -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
+ 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)))