linear-algebra-toolkit 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.
- linear_algebra_toolkit-0.1.0/LICENSE +21 -0
- linear_algebra_toolkit-0.1.0/MANIFEST.in +4 -0
- linear_algebra_toolkit-0.1.0/PKG-INFO +140 -0
- linear_algebra_toolkit-0.1.0/README.md +117 -0
- linear_algebra_toolkit-0.1.0/examples/matrix_dot_vector.py +10 -0
- linear_algebra_toolkit-0.1.0/examples/matrix_plus_matrix.py +21 -0
- linear_algebra_toolkit-0.1.0/examples/matrix_plus_vector.py +14 -0
- linear_algebra_toolkit-0.1.0/examples/vector_as_matrix.py +10 -0
- linear_algebra_toolkit-0.1.0/examples/vector_dot_matrix.py +9 -0
- linear_algebra_toolkit-0.1.0/examples/vector_dot_product.py +11 -0
- linear_algebra_toolkit-0.1.0/examples/vector_minus_vector.py +11 -0
- linear_algebra_toolkit-0.1.0/examples/vector_plus_matrix.py +14 -0
- linear_algebra_toolkit-0.1.0/examples/vector_plus_vector.py +11 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit/__init__.py +1 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit/objects.py +393 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit/utils.py +26 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit.egg-info/PKG-INFO +140 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit.egg-info/SOURCES.txt +22 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit.egg-info/dependency_links.txt +1 -0
- linear_algebra_toolkit-0.1.0/linear_algebra_toolkit.egg-info/top_level.txt +1 -0
- linear_algebra_toolkit-0.1.0/pyproject.toml +35 -0
- linear_algebra_toolkit-0.1.0/setup.cfg +4 -0
- linear_algebra_toolkit-0.1.0/tests/test_objects.py +205 -0
- linear_algebra_toolkit-0.1.0/tests/test_utils.py +19 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max B.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linear-algebra-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Educational pure-Python vector and matrix objects for linear algebra experiments.
|
|
5
|
+
Author: Max B.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/maxboro/linear-algebra-toolkit
|
|
8
|
+
Project-URL: Repository, https://github.com/maxboro/linear-algebra-toolkit
|
|
9
|
+
Project-URL: Issues, https://github.com/maxboro/linear-algebra-toolkit/issues
|
|
10
|
+
Keywords: linear algebra,matrix,vector,education,math
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Education
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Linear Algebra Toolkit
|
|
25
|
+
|
|
26
|
+
`linear-algebra-toolkit` is a small pure-Python package for experimenting with
|
|
27
|
+
core linear algebra operations without NumPy or other numerical libraries.
|
|
28
|
+
|
|
29
|
+
The codebase is intentionally simple and explicit. It is best suited for
|
|
30
|
+
learning, reading through the implementation, and running small examples rather
|
|
31
|
+
than for high-performance scientific computing.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the published package from PyPI with:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install linear-algebra-toolkit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you are working from a local checkout, install the project in editable mode
|
|
42
|
+
with:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python -m pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What the project provides
|
|
49
|
+
|
|
50
|
+
- A `Vector` type for one-dimensional numeric data.
|
|
51
|
+
- A `Matrix` type for two-dimensional numeric data.
|
|
52
|
+
- Operator overloads for addition, subtraction, element-wise multiplication and
|
|
53
|
+
division, and linear algebra products through `@`.
|
|
54
|
+
- A couple of small utility helpers used by the core objects.
|
|
55
|
+
- No runtime dependencies outside the Python standard library.
|
|
56
|
+
|
|
57
|
+
## Design goals
|
|
58
|
+
|
|
59
|
+
- Keep the implementation readable and easy to inspect.
|
|
60
|
+
- Avoid hidden abstractions so the math stays visible in plain Python.
|
|
61
|
+
- Provide a compact codebase that is easy to test and extend.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
Import the public objects directly from the package:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from linear_algebra_toolkit import Matrix, Vector
|
|
69
|
+
|
|
70
|
+
vector = Vector([3, 4])
|
|
71
|
+
other = Vector([1, 2])
|
|
72
|
+
matrix = Matrix([[1, 2], [3, 4]])
|
|
73
|
+
|
|
74
|
+
print(vector + other) # Vector([4, 6], n elements=2)
|
|
75
|
+
print(vector @ other) # 11
|
|
76
|
+
print(matrix @ vector) # Vector([11, 25], n elements=2)
|
|
77
|
+
print(vector.normalized) # Vector([0.6, 0.8], n elements=2)
|
|
78
|
+
print(vector.as_row_matrix())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Supported operations
|
|
82
|
+
|
|
83
|
+
### Vector
|
|
84
|
+
|
|
85
|
+
- Construction from a non-empty list of `int` and `float` values.
|
|
86
|
+
- `+` and `-` with vectors of the same shape.
|
|
87
|
+
- `+` and `-` with compatible row matrices (`1 x n`) and column matrices (`n x 1`).
|
|
88
|
+
- `*` and `/` with scalars.
|
|
89
|
+
- Element-wise `*` and `/` with another vector of the same shape.
|
|
90
|
+
- `@` with another vector for the dot product.
|
|
91
|
+
- `@` with a compatible matrix.
|
|
92
|
+
- Norms through `norm(mode="euclidean" | "manhattan" | "max")`.
|
|
93
|
+
- `normalized`, `abs(vector)`, `round(vector, ndigits)`,
|
|
94
|
+
`as_row_matrix()`, and `as_col_matrix()`.
|
|
95
|
+
|
|
96
|
+
### Matrix
|
|
97
|
+
|
|
98
|
+
- Construction from a non-empty rectangular list of rows.
|
|
99
|
+
- `+` and `-` with matrices of the same shape.
|
|
100
|
+
- `+` and `-` with compatible vectors represented as a row or column.
|
|
101
|
+
- `*` and `/` with scalars.
|
|
102
|
+
- Element-wise `*` and `/` with another matrix of the same shape.
|
|
103
|
+
- `@` with another compatible matrix.
|
|
104
|
+
- `@` with a compatible vector.
|
|
105
|
+
- `T` for transpose, `norm()` for the Frobenius norm,
|
|
106
|
+
`get_row_elements()`, `get_col_elements()`, and `get_flatten_elements()`.
|
|
107
|
+
|
|
108
|
+
## Behavior notes
|
|
109
|
+
|
|
110
|
+
- Only plain Python `int` and `float` values are accepted.
|
|
111
|
+
- Division goes through `zero_aware_division()`. With the current default
|
|
112
|
+
configuration, dividing by zero returns `0` instead of raising an exception.
|
|
113
|
+
- The project is educational in scope and prioritizes clarity over performance.
|
|
114
|
+
|
|
115
|
+
## Running tests
|
|
116
|
+
|
|
117
|
+
From a repository checkout:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m unittest discover -s tests
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Repository examples
|
|
124
|
+
|
|
125
|
+
The repository includes a few small scripts in `examples/` that demonstrate the
|
|
126
|
+
API. From the repository root you can run them like this:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m examples.vector_plus_vector
|
|
130
|
+
python -m examples.vector_dot_product
|
|
131
|
+
python -m examples.matrix_dot_vector
|
|
132
|
+
python -m examples.vector_as_matrix
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Repository layout
|
|
136
|
+
|
|
137
|
+
- `linear_algebra_toolkit/objects.py` contains the `Vector` and `Matrix` implementations.
|
|
138
|
+
- `linear_algebra_toolkit/utils.py` contains small shared helpers.
|
|
139
|
+
- `tests/` contains the unit tests for the public behavior.
|
|
140
|
+
- `examples/` contains runnable usage examples.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Linear Algebra Toolkit
|
|
2
|
+
|
|
3
|
+
`linear-algebra-toolkit` is a small pure-Python package for experimenting with
|
|
4
|
+
core linear algebra operations without NumPy or other numerical libraries.
|
|
5
|
+
|
|
6
|
+
The codebase is intentionally simple and explicit. It is best suited for
|
|
7
|
+
learning, reading through the implementation, and running small examples rather
|
|
8
|
+
than for high-performance scientific computing.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Install the published package from PyPI with:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python -m pip install linear-algebra-toolkit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If you are working from a local checkout, install the project in editable mode
|
|
19
|
+
with:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What the project provides
|
|
26
|
+
|
|
27
|
+
- A `Vector` type for one-dimensional numeric data.
|
|
28
|
+
- A `Matrix` type for two-dimensional numeric data.
|
|
29
|
+
- Operator overloads for addition, subtraction, element-wise multiplication and
|
|
30
|
+
division, and linear algebra products through `@`.
|
|
31
|
+
- A couple of small utility helpers used by the core objects.
|
|
32
|
+
- No runtime dependencies outside the Python standard library.
|
|
33
|
+
|
|
34
|
+
## Design goals
|
|
35
|
+
|
|
36
|
+
- Keep the implementation readable and easy to inspect.
|
|
37
|
+
- Avoid hidden abstractions so the math stays visible in plain Python.
|
|
38
|
+
- Provide a compact codebase that is easy to test and extend.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Import the public objects directly from the package:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from linear_algebra_toolkit import Matrix, Vector
|
|
46
|
+
|
|
47
|
+
vector = Vector([3, 4])
|
|
48
|
+
other = Vector([1, 2])
|
|
49
|
+
matrix = Matrix([[1, 2], [3, 4]])
|
|
50
|
+
|
|
51
|
+
print(vector + other) # Vector([4, 6], n elements=2)
|
|
52
|
+
print(vector @ other) # 11
|
|
53
|
+
print(matrix @ vector) # Vector([11, 25], n elements=2)
|
|
54
|
+
print(vector.normalized) # Vector([0.6, 0.8], n elements=2)
|
|
55
|
+
print(vector.as_row_matrix())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Supported operations
|
|
59
|
+
|
|
60
|
+
### Vector
|
|
61
|
+
|
|
62
|
+
- Construction from a non-empty list of `int` and `float` values.
|
|
63
|
+
- `+` and `-` with vectors of the same shape.
|
|
64
|
+
- `+` and `-` with compatible row matrices (`1 x n`) and column matrices (`n x 1`).
|
|
65
|
+
- `*` and `/` with scalars.
|
|
66
|
+
- Element-wise `*` and `/` with another vector of the same shape.
|
|
67
|
+
- `@` with another vector for the dot product.
|
|
68
|
+
- `@` with a compatible matrix.
|
|
69
|
+
- Norms through `norm(mode="euclidean" | "manhattan" | "max")`.
|
|
70
|
+
- `normalized`, `abs(vector)`, `round(vector, ndigits)`,
|
|
71
|
+
`as_row_matrix()`, and `as_col_matrix()`.
|
|
72
|
+
|
|
73
|
+
### Matrix
|
|
74
|
+
|
|
75
|
+
- Construction from a non-empty rectangular list of rows.
|
|
76
|
+
- `+` and `-` with matrices of the same shape.
|
|
77
|
+
- `+` and `-` with compatible vectors represented as a row or column.
|
|
78
|
+
- `*` and `/` with scalars.
|
|
79
|
+
- Element-wise `*` and `/` with another matrix of the same shape.
|
|
80
|
+
- `@` with another compatible matrix.
|
|
81
|
+
- `@` with a compatible vector.
|
|
82
|
+
- `T` for transpose, `norm()` for the Frobenius norm,
|
|
83
|
+
`get_row_elements()`, `get_col_elements()`, and `get_flatten_elements()`.
|
|
84
|
+
|
|
85
|
+
## Behavior notes
|
|
86
|
+
|
|
87
|
+
- Only plain Python `int` and `float` values are accepted.
|
|
88
|
+
- Division goes through `zero_aware_division()`. With the current default
|
|
89
|
+
configuration, dividing by zero returns `0` instead of raising an exception.
|
|
90
|
+
- The project is educational in scope and prioritizes clarity over performance.
|
|
91
|
+
|
|
92
|
+
## Running tests
|
|
93
|
+
|
|
94
|
+
From a repository checkout:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m unittest discover -s tests
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Repository examples
|
|
101
|
+
|
|
102
|
+
The repository includes a few small scripts in `examples/` that demonstrate the
|
|
103
|
+
API. From the repository root you can run them like this:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python -m examples.vector_plus_vector
|
|
107
|
+
python -m examples.vector_dot_product
|
|
108
|
+
python -m examples.matrix_dot_vector
|
|
109
|
+
python -m examples.vector_as_matrix
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Repository layout
|
|
113
|
+
|
|
114
|
+
- `linear_algebra_toolkit/objects.py` contains the `Vector` and `Matrix` implementations.
|
|
115
|
+
- `linear_algebra_toolkit/utils.py` contains small shared helpers.
|
|
116
|
+
- `tests/` contains the unit tests for the public behavior.
|
|
117
|
+
- `examples/` contains runnable usage examples.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
python -m examples.matrix_plus_matrix
|
|
3
|
+
"""
|
|
4
|
+
from linear_algebra_toolkit import Matrix
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
mat1 = Matrix([
|
|
8
|
+
[1, 2, 3],
|
|
9
|
+
[1, 2, 3]
|
|
10
|
+
])
|
|
11
|
+
mat2 = Matrix([
|
|
12
|
+
[1, 0, 3],
|
|
13
|
+
[1, 0, 3]
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
print("vec1 + mat1", mat1 + mat2)
|
|
17
|
+
print("vec1 + mat2", mat1 + mat2)
|
|
18
|
+
|
|
19
|
+
print("vec1 - mat1", mat1 - mat2)
|
|
20
|
+
print("vec1 - mat2", mat1 - mat2)
|
|
21
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
python -m examples.matrix_plus_vector
|
|
3
|
+
"""
|
|
4
|
+
from linear_algebra_toolkit import Vector, Matrix
|
|
5
|
+
|
|
6
|
+
vec1 = Vector([1, 2, 3])
|
|
7
|
+
mat1 = Matrix([[1, 2, 3]])
|
|
8
|
+
mat2 = Matrix([[1], [2], [3]])
|
|
9
|
+
|
|
10
|
+
print("mat1 + vec1", mat1 + vec1)
|
|
11
|
+
print("mat2 + vec1", mat2 + vec1)
|
|
12
|
+
|
|
13
|
+
print("mat1 - vec1", mat1 - vec1)
|
|
14
|
+
print("mat2 - vec1", mat2 - vec1)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
python -m examples.vector_plus_matrix
|
|
3
|
+
"""
|
|
4
|
+
from linear_algebra_toolkit import Vector, Matrix
|
|
5
|
+
|
|
6
|
+
vec1 = Vector([1, 2, 3])
|
|
7
|
+
mat1 = Matrix([[1, 2, 3]])
|
|
8
|
+
mat2 = Matrix([[1], [2], [3]])
|
|
9
|
+
|
|
10
|
+
print("vec1 + mat1", vec1 + mat1)
|
|
11
|
+
print("vec1 + mat2", vec1 + mat2)
|
|
12
|
+
|
|
13
|
+
print("vec1 - mat1", vec1 - mat1)
|
|
14
|
+
print("vec1 - mat2", vec1 - mat2)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .objects import Vector, Matrix
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Core linear algebra objects used throughout the toolkit.
|
|
2
|
+
|
|
3
|
+
The module exposes lightweight ``Vector`` and ``Matrix`` classes designed for
|
|
4
|
+
educational use and implemented without third-party numerical libraries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, List
|
|
10
|
+
|
|
11
|
+
from .utils import zero_aware_division, vector_dot_product
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LAObject(ABC):
|
|
15
|
+
"""Base class for objects that support additive arithmetic."""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def additive_operation(self, other, sign):
|
|
19
|
+
"""Implement addition or subtraction against another object.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
other: The value to combine with ``self``.
|
|
23
|
+
sign: ``1`` for addition and ``-1`` for subtraction.
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def __add__(self, other):
|
|
28
|
+
return self.additive_operation(other, sign = 1)
|
|
29
|
+
|
|
30
|
+
def __sub__(self, other):
|
|
31
|
+
return self.additive_operation(other, sign = -1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Vector(LAObject):
|
|
35
|
+
"""One-dimensional numeric object with basic linear algebra operations.
|
|
36
|
+
|
|
37
|
+
Vectors support:
|
|
38
|
+
- addition and subtraction with vectors of the same length
|
|
39
|
+
- addition and subtraction with ``1 x n`` or ``n x 1`` matrices
|
|
40
|
+
- scalar and element-wise multiplication and division
|
|
41
|
+
- dot products with vectors and multiplication by compatible matrices
|
|
42
|
+
- common norms, normalization, and conversion to matrix form
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, vector_elements: List[Any]):
|
|
46
|
+
"""Create a vector from a non-empty list of numeric values."""
|
|
47
|
+
if not isinstance(vector_elements, list):
|
|
48
|
+
raise TypeError("Should be a list")
|
|
49
|
+
if len(vector_elements) == 0:
|
|
50
|
+
raise ValueError("Empty vectors are not allowed")
|
|
51
|
+
|
|
52
|
+
for element in vector_elements:
|
|
53
|
+
if not isinstance(element, (int, float)):
|
|
54
|
+
raise TypeError("Should be an int or float")
|
|
55
|
+
|
|
56
|
+
self.n = len(vector_elements)
|
|
57
|
+
self._vector_elements = vector_elements
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def vector_elements(self):
|
|
61
|
+
"""Return the stored vector elements."""
|
|
62
|
+
return self._vector_elements
|
|
63
|
+
|
|
64
|
+
def __repr__(self):
|
|
65
|
+
return f"Vector({self.vector_elements}, n elements={self.n})"
|
|
66
|
+
|
|
67
|
+
def _require_other_vector(self, other):
|
|
68
|
+
if not isinstance(other, Vector):
|
|
69
|
+
raise RuntimeError("Operation is possible only among Vectors")
|
|
70
|
+
|
|
71
|
+
def _require_similar_shape(self, other):
|
|
72
|
+
if self.n != other.n:
|
|
73
|
+
raise RuntimeError(f"Need to be with similar shapes {self.n} != {other.n}")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _create_none_list(n):
|
|
77
|
+
new_list = [None for _ in range(n)]
|
|
78
|
+
return new_list
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def shape(self):
|
|
82
|
+
"""Return the vector shape as a one-item tuple."""
|
|
83
|
+
return (self.n,)
|
|
84
|
+
|
|
85
|
+
def __eq__(self, other):
|
|
86
|
+
if not isinstance(other, Vector):
|
|
87
|
+
return NotImplemented
|
|
88
|
+
return self.vector_elements == other.vector_elements
|
|
89
|
+
|
|
90
|
+
def __ne__(self, other):
|
|
91
|
+
if not isinstance(other, Vector):
|
|
92
|
+
return NotImplemented
|
|
93
|
+
return self.vector_elements != other.vector_elements
|
|
94
|
+
|
|
95
|
+
def __round__(self, ndigits: int = None):
|
|
96
|
+
result = self._create_none_list(self.n)
|
|
97
|
+
for n_ind in range(self.n):
|
|
98
|
+
result[n_ind] = round(self.vector_elements[n_ind], ndigits)
|
|
99
|
+
return Vector(result)
|
|
100
|
+
|
|
101
|
+
def additive_operation(self, other, sign):
|
|
102
|
+
if isinstance(other, Vector):
|
|
103
|
+
self._require_similar_shape(other)
|
|
104
|
+
|
|
105
|
+
result = self._create_none_list(self.n)
|
|
106
|
+
for n_ind in range(self.n):
|
|
107
|
+
result[n_ind] = self.vector_elements[n_ind] + sign * other.vector_elements[n_ind]
|
|
108
|
+
return Vector(result)
|
|
109
|
+
elif isinstance(other, Matrix):
|
|
110
|
+
if other.shape[0] == 1:
|
|
111
|
+
return self.as_row_matrix() + sign * other
|
|
112
|
+
elif other.shape[1] == 1:
|
|
113
|
+
return self.as_col_matrix() + sign * other
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError(f"Operation cannot be performed for shape of other {other.shape}, must be mx1, 1xn")
|
|
116
|
+
else:
|
|
117
|
+
raise TypeError("Other should be Vector or Matrix")
|
|
118
|
+
|
|
119
|
+
def __mul__(self, other):
|
|
120
|
+
result = self._create_none_list(self.n)
|
|
121
|
+
if isinstance(other, (int, float)):
|
|
122
|
+
for n_ind in range(self.n):
|
|
123
|
+
result[n_ind] = self.vector_elements[n_ind] * other
|
|
124
|
+
elif isinstance(other, Vector):
|
|
125
|
+
# Hadamard product
|
|
126
|
+
self._require_similar_shape(other)
|
|
127
|
+
for n_ind in range(self.n):
|
|
128
|
+
result[n_ind] = self.vector_elements[n_ind] * other.vector_elements[n_ind]
|
|
129
|
+
else:
|
|
130
|
+
raise TypeError("Not supported")
|
|
131
|
+
return Vector(result)
|
|
132
|
+
|
|
133
|
+
def __truediv__(self, other):
|
|
134
|
+
result = self._create_none_list(self.n)
|
|
135
|
+
if isinstance(other, (int, float)):
|
|
136
|
+
for n_ind in range(self.n):
|
|
137
|
+
result[n_ind] = zero_aware_division(self.vector_elements[n_ind], other)
|
|
138
|
+
elif isinstance(other, Vector):
|
|
139
|
+
# element wise
|
|
140
|
+
self._require_similar_shape(other)
|
|
141
|
+
for n_ind in range(self.n):
|
|
142
|
+
result[n_ind] = zero_aware_division(self.vector_elements[n_ind], other.vector_elements[n_ind])
|
|
143
|
+
else:
|
|
144
|
+
raise TypeError("Not supported")
|
|
145
|
+
return Vector(result)
|
|
146
|
+
|
|
147
|
+
def __rmul__(self, other):
|
|
148
|
+
return self.__mul__(other)
|
|
149
|
+
|
|
150
|
+
def __matmul__(self, other):
|
|
151
|
+
if isinstance(other, Vector):
|
|
152
|
+
return vector_dot_product(self.vector_elements, other.vector_elements)
|
|
153
|
+
elif isinstance(other, Matrix):
|
|
154
|
+
if self.n != other.m:
|
|
155
|
+
raise ValueError(f"{self.n} != {other.m}")
|
|
156
|
+
result_elements = []
|
|
157
|
+
for ind in range(other.n):
|
|
158
|
+
matrix_row_elements = other.get_col_elements(ind)
|
|
159
|
+
row_dot_product = vector_dot_product(self.vector_elements, matrix_row_elements)
|
|
160
|
+
result_elements.append(row_dot_product)
|
|
161
|
+
return Vector(result_elements)
|
|
162
|
+
else:
|
|
163
|
+
raise TypeError("Other should be Matrix or Vector")
|
|
164
|
+
|
|
165
|
+
def norm(self, mode="euclidean"):
|
|
166
|
+
"""Return the vector norm for the requested mode.
|
|
167
|
+
|
|
168
|
+
Supported modes are ``"euclidean"``, ``"manhattan"``, and ``"max"``.
|
|
169
|
+
"""
|
|
170
|
+
if mode == "euclidean":
|
|
171
|
+
return math.sqrt(sum([x**2 for x in self.vector_elements]))
|
|
172
|
+
elif mode == "manhattan":
|
|
173
|
+
return sum([abs(x) for x in self.vector_elements])
|
|
174
|
+
elif mode == "max":
|
|
175
|
+
return max([abs(x) for x in self.vector_elements])
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError("Mode can be 'euclidean' or 'manhattan' or 'max'")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def __len__(self):
|
|
181
|
+
return len(self.vector_elements)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def normalized(self):
|
|
185
|
+
"""Return a copy scaled to have Euclidean norm equal to ``1``."""
|
|
186
|
+
current_norm = self.norm(mode="euclidean")
|
|
187
|
+
if current_norm == 0:
|
|
188
|
+
raise ValueError("Cannot normalize zero vector")
|
|
189
|
+
norm_coef = 1 / current_norm
|
|
190
|
+
return self.__mul__(norm_coef)
|
|
191
|
+
|
|
192
|
+
def __abs__(self):
|
|
193
|
+
new_vector_elements = [abs(x) for x in self.vector_elements]
|
|
194
|
+
return Vector(new_vector_elements)
|
|
195
|
+
|
|
196
|
+
def as_row_matrix(self) -> "Matrix":
|
|
197
|
+
"""Return the vector as a ``1 x n`` matrix."""
|
|
198
|
+
return Matrix([self.vector_elements.copy()])
|
|
199
|
+
|
|
200
|
+
def as_col_matrix(self) -> "Matrix":
|
|
201
|
+
"""Return the vector as a ``n x 1`` matrix."""
|
|
202
|
+
matrix_elements = [[element] for element in self.vector_elements]
|
|
203
|
+
return Matrix(matrix_elements)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Matrix(LAObject):
|
|
207
|
+
"""Two-dimensional numeric object with basic matrix operations.
|
|
208
|
+
|
|
209
|
+
Matrices support:
|
|
210
|
+
- addition and subtraction with matrices of the same shape
|
|
211
|
+
- addition and subtraction with compatible row or column vectors
|
|
212
|
+
- scalar and element-wise multiplication and division
|
|
213
|
+
- matrix-matrix and matrix-vector multiplication
|
|
214
|
+
- transpose, flattening, absolute value, and the Frobenius norm
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, matrix_elements: List[List[Any]]):
|
|
218
|
+
"""Create a matrix from a non-empty rectangular list of rows."""
|
|
219
|
+
if not isinstance(matrix_elements, list):
|
|
220
|
+
raise TypeError("Should be a list")
|
|
221
|
+
if len(matrix_elements) == 0:
|
|
222
|
+
raise ValueError("Empty matrixes are not allowed")
|
|
223
|
+
|
|
224
|
+
n = None
|
|
225
|
+
for row in matrix_elements:
|
|
226
|
+
if not isinstance(row, list):
|
|
227
|
+
raise TypeError("Should be a list")
|
|
228
|
+
if len(row) == 0:
|
|
229
|
+
raise ValueError("Empty matrix are not allowed")
|
|
230
|
+
for element in row:
|
|
231
|
+
if not isinstance(element, (int, float)):
|
|
232
|
+
raise TypeError("Should be an int or float")
|
|
233
|
+
if n is None:
|
|
234
|
+
n = len(row)
|
|
235
|
+
elif n != len(row):
|
|
236
|
+
raise ValueError("incorrect shape")
|
|
237
|
+
|
|
238
|
+
self.m = len(matrix_elements)
|
|
239
|
+
self.n = n
|
|
240
|
+
self._matrix_elements = matrix_elements
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def matrix_elements(self):
|
|
244
|
+
"""Return the stored matrix elements."""
|
|
245
|
+
return self._matrix_elements
|
|
246
|
+
|
|
247
|
+
def __repr__(self):
|
|
248
|
+
return f"Matrix({self.matrix_elements}, shape={self.m}x{self.n})"
|
|
249
|
+
|
|
250
|
+
def _require_other_matrix(self, other):
|
|
251
|
+
if not isinstance(other, Matrix):
|
|
252
|
+
raise RuntimeError("Operation is possible only among Matrixes")
|
|
253
|
+
|
|
254
|
+
def _require_similar_shape(self, other):
|
|
255
|
+
if self.shape != other.shape:
|
|
256
|
+
raise RuntimeError(f"Need to be with similar shapes {self.shape} != {other.shape}")
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _create_none_list(m, n):
|
|
260
|
+
new_list = [[None for _ in range(n)] for _ in range(m)]
|
|
261
|
+
return new_list
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def shape(self):
|
|
265
|
+
"""Return the matrix shape as ``(rows, columns)``."""
|
|
266
|
+
return (self.m, self.n)
|
|
267
|
+
|
|
268
|
+
def __eq__(self, other):
|
|
269
|
+
if not isinstance(other, Matrix):
|
|
270
|
+
return NotImplemented
|
|
271
|
+
return self.matrix_elements == other.matrix_elements
|
|
272
|
+
|
|
273
|
+
def __ne__(self, other):
|
|
274
|
+
if not isinstance(other, Matrix):
|
|
275
|
+
return NotImplemented
|
|
276
|
+
return self.matrix_elements != other.matrix_elements
|
|
277
|
+
|
|
278
|
+
def __round__(self, ndigits: int = None):
|
|
279
|
+
result = self._create_none_list(self.m, self.n)
|
|
280
|
+
for m_ind in range(self.m):
|
|
281
|
+
for n_ind in range(self.n):
|
|
282
|
+
result[m_ind][n_ind] = round(self.matrix_elements[m_ind][n_ind], ndigits)
|
|
283
|
+
return Matrix(result)
|
|
284
|
+
|
|
285
|
+
def additive_operation(self, other, sign):
|
|
286
|
+
if isinstance(other, Vector):
|
|
287
|
+
return sign * other + self
|
|
288
|
+
elif isinstance(other, Matrix):
|
|
289
|
+
self._require_similar_shape(other)
|
|
290
|
+
|
|
291
|
+
result = self._create_none_list(self.m, self.n)
|
|
292
|
+
for m_ind in range(self.m):
|
|
293
|
+
for n_ind in range(self.n):
|
|
294
|
+
result[m_ind][n_ind] = self.matrix_elements[m_ind][n_ind] + sign * other.matrix_elements[m_ind][n_ind]
|
|
295
|
+
return Matrix(result)
|
|
296
|
+
else:
|
|
297
|
+
raise TypeError("Other should be Vector or Matrix")
|
|
298
|
+
|
|
299
|
+
def __mul__(self, other):
|
|
300
|
+
result = self._create_none_list(self.m, self.n)
|
|
301
|
+
if isinstance(other, (int, float)):
|
|
302
|
+
for m_ind in range(self.m):
|
|
303
|
+
for n_ind in range(self.n):
|
|
304
|
+
result[m_ind][n_ind] = self.matrix_elements[m_ind][n_ind] * other
|
|
305
|
+
elif isinstance(other, Matrix):
|
|
306
|
+
# Hadamard product
|
|
307
|
+
self._require_similar_shape(other)
|
|
308
|
+
for m_ind in range(self.m):
|
|
309
|
+
for n_ind in range(self.n):
|
|
310
|
+
result[m_ind][n_ind] = self.matrix_elements[m_ind][n_ind] * other.matrix_elements[m_ind][n_ind]
|
|
311
|
+
else:
|
|
312
|
+
raise TypeError("Not supported")
|
|
313
|
+
return Matrix(result)
|
|
314
|
+
|
|
315
|
+
def __truediv__(self, other):
|
|
316
|
+
result = self._create_none_list(self.m, self.n)
|
|
317
|
+
if isinstance(other, (int, float)):
|
|
318
|
+
for m_ind in range(self.m):
|
|
319
|
+
for n_ind in range(self.n):
|
|
320
|
+
result[m_ind][n_ind] = zero_aware_division(self.matrix_elements[m_ind][n_ind], other)
|
|
321
|
+
elif isinstance(other, Matrix):
|
|
322
|
+
self._require_similar_shape(other)
|
|
323
|
+
for m_ind in range(self.m):
|
|
324
|
+
for n_ind in range(self.n):
|
|
325
|
+
result[m_ind][n_ind] = zero_aware_division(self.matrix_elements[m_ind][n_ind], other.matrix_elements[m_ind][n_ind])
|
|
326
|
+
else:
|
|
327
|
+
raise TypeError("Not supported")
|
|
328
|
+
return Matrix(result)
|
|
329
|
+
|
|
330
|
+
def __rmul__(self, other):
|
|
331
|
+
return self.__mul__(other)
|
|
332
|
+
|
|
333
|
+
def get_row_elements(self, index: int) -> list:
|
|
334
|
+
"""Return a copy of the row at ``index``."""
|
|
335
|
+
return self.matrix_elements[index].copy()
|
|
336
|
+
|
|
337
|
+
def get_col_elements(self, index: int) -> list:
|
|
338
|
+
"""Return a copy of the column at ``index``."""
|
|
339
|
+
col_elements = []
|
|
340
|
+
for row in self.matrix_elements:
|
|
341
|
+
col_elements.append(row[index])
|
|
342
|
+
return col_elements
|
|
343
|
+
|
|
344
|
+
def __matmul__(self, other):
|
|
345
|
+
if isinstance(other, Matrix):
|
|
346
|
+
if self.n != other.m:
|
|
347
|
+
raise RuntimeError(f"Incompatible shape {self.n} != {other.m}")
|
|
348
|
+
|
|
349
|
+
result = self._create_none_list(self.m, other.n)
|
|
350
|
+
for m_ind in range(self.m):
|
|
351
|
+
for n_ind in range(other.n):
|
|
352
|
+
self_row = self.get_row_elements(m_ind)
|
|
353
|
+
other_col = other.get_col_elements(n_ind)
|
|
354
|
+
dot_product = vector_dot_product(self_row, other_col)
|
|
355
|
+
result[m_ind][n_ind] = dot_product
|
|
356
|
+
return Matrix(result)
|
|
357
|
+
elif isinstance(other, Vector):
|
|
358
|
+
if self.n != other.n:
|
|
359
|
+
raise ValueError(f"{self.n} != {other.n}")
|
|
360
|
+
result_elements = []
|
|
361
|
+
for self_row in self.matrix_elements:
|
|
362
|
+
row_dot_product = vector_dot_product(self_row, other.vector_elements)
|
|
363
|
+
result_elements.append(row_dot_product)
|
|
364
|
+
return Vector(result_elements)
|
|
365
|
+
else:
|
|
366
|
+
raise TypeError("Other should be Matrix or Vector")
|
|
367
|
+
|
|
368
|
+
def get_flatten_elements(self) -> list:
|
|
369
|
+
"""Return the matrix elements flattened in row-major order."""
|
|
370
|
+
elements = []
|
|
371
|
+
for row in self.matrix_elements:
|
|
372
|
+
for value in row:
|
|
373
|
+
elements.append(value)
|
|
374
|
+
return elements
|
|
375
|
+
|
|
376
|
+
def norm(self):
|
|
377
|
+
"""Return the Frobenius norm of the matrix."""
|
|
378
|
+
flatten = self.get_flatten_elements()
|
|
379
|
+
result = math.sqrt(sum([x**2 for x in flatten]))
|
|
380
|
+
return result
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def T(self):
|
|
384
|
+
"""Return the transpose of the matrix."""
|
|
385
|
+
result = self._create_none_list(self.n, self.m)
|
|
386
|
+
for m_ind in range(self.m):
|
|
387
|
+
for n_ind in range(self.n):
|
|
388
|
+
result[n_ind][m_ind] = self.matrix_elements[m_ind][n_ind]
|
|
389
|
+
return Matrix(result)
|
|
390
|
+
|
|
391
|
+
def __abs__(self):
|
|
392
|
+
new_vector_elements = [[abs(x) for x in row] for row in self.matrix_elements]
|
|
393
|
+
return Matrix(new_vector_elements)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Utility helpers shared by the vector and matrix implementations."""
|
|
2
|
+
|
|
3
|
+
ZERO_DIVISION_TO_ZERO = True
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def zero_aware_division(dividend, divisor):
|
|
7
|
+
"""Divide two numbers while handling zero divisors consistently.
|
|
8
|
+
|
|
9
|
+
When ``ZERO_DIVISION_TO_ZERO`` is enabled, dividing by zero returns ``0``
|
|
10
|
+
instead of raising ``ZeroDivisionError``.
|
|
11
|
+
"""
|
|
12
|
+
if divisor == 0 and ZERO_DIVISION_TO_ZERO:
|
|
13
|
+
return 0
|
|
14
|
+
else:
|
|
15
|
+
return dividend / divisor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def vector_dot_product(vec1: list, vec2: list):
|
|
19
|
+
"""Return the dot product of two equally sized numeric sequences."""
|
|
20
|
+
if len(vec1) != len(vec2):
|
|
21
|
+
raise RuntimeError(f"Vector len should be equal {len(vec1)} != {len(vec2)}")
|
|
22
|
+
|
|
23
|
+
result = 0
|
|
24
|
+
for ind in range(len(vec1)):
|
|
25
|
+
result += vec1[ind] * vec2[ind]
|
|
26
|
+
return result
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linear-algebra-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Educational pure-Python vector and matrix objects for linear algebra experiments.
|
|
5
|
+
Author: Max B.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/maxboro/linear-algebra-toolkit
|
|
8
|
+
Project-URL: Repository, https://github.com/maxboro/linear-algebra-toolkit
|
|
9
|
+
Project-URL: Issues, https://github.com/maxboro/linear-algebra-toolkit/issues
|
|
10
|
+
Keywords: linear algebra,matrix,vector,education,math
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Education
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Linear Algebra Toolkit
|
|
25
|
+
|
|
26
|
+
`linear-algebra-toolkit` is a small pure-Python package for experimenting with
|
|
27
|
+
core linear algebra operations without NumPy or other numerical libraries.
|
|
28
|
+
|
|
29
|
+
The codebase is intentionally simple and explicit. It is best suited for
|
|
30
|
+
learning, reading through the implementation, and running small examples rather
|
|
31
|
+
than for high-performance scientific computing.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the published package from PyPI with:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install linear-algebra-toolkit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you are working from a local checkout, install the project in editable mode
|
|
42
|
+
with:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python -m pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What the project provides
|
|
49
|
+
|
|
50
|
+
- A `Vector` type for one-dimensional numeric data.
|
|
51
|
+
- A `Matrix` type for two-dimensional numeric data.
|
|
52
|
+
- Operator overloads for addition, subtraction, element-wise multiplication and
|
|
53
|
+
division, and linear algebra products through `@`.
|
|
54
|
+
- A couple of small utility helpers used by the core objects.
|
|
55
|
+
- No runtime dependencies outside the Python standard library.
|
|
56
|
+
|
|
57
|
+
## Design goals
|
|
58
|
+
|
|
59
|
+
- Keep the implementation readable and easy to inspect.
|
|
60
|
+
- Avoid hidden abstractions so the math stays visible in plain Python.
|
|
61
|
+
- Provide a compact codebase that is easy to test and extend.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
Import the public objects directly from the package:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from linear_algebra_toolkit import Matrix, Vector
|
|
69
|
+
|
|
70
|
+
vector = Vector([3, 4])
|
|
71
|
+
other = Vector([1, 2])
|
|
72
|
+
matrix = Matrix([[1, 2], [3, 4]])
|
|
73
|
+
|
|
74
|
+
print(vector + other) # Vector([4, 6], n elements=2)
|
|
75
|
+
print(vector @ other) # 11
|
|
76
|
+
print(matrix @ vector) # Vector([11, 25], n elements=2)
|
|
77
|
+
print(vector.normalized) # Vector([0.6, 0.8], n elements=2)
|
|
78
|
+
print(vector.as_row_matrix())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Supported operations
|
|
82
|
+
|
|
83
|
+
### Vector
|
|
84
|
+
|
|
85
|
+
- Construction from a non-empty list of `int` and `float` values.
|
|
86
|
+
- `+` and `-` with vectors of the same shape.
|
|
87
|
+
- `+` and `-` with compatible row matrices (`1 x n`) and column matrices (`n x 1`).
|
|
88
|
+
- `*` and `/` with scalars.
|
|
89
|
+
- Element-wise `*` and `/` with another vector of the same shape.
|
|
90
|
+
- `@` with another vector for the dot product.
|
|
91
|
+
- `@` with a compatible matrix.
|
|
92
|
+
- Norms through `norm(mode="euclidean" | "manhattan" | "max")`.
|
|
93
|
+
- `normalized`, `abs(vector)`, `round(vector, ndigits)`,
|
|
94
|
+
`as_row_matrix()`, and `as_col_matrix()`.
|
|
95
|
+
|
|
96
|
+
### Matrix
|
|
97
|
+
|
|
98
|
+
- Construction from a non-empty rectangular list of rows.
|
|
99
|
+
- `+` and `-` with matrices of the same shape.
|
|
100
|
+
- `+` and `-` with compatible vectors represented as a row or column.
|
|
101
|
+
- `*` and `/` with scalars.
|
|
102
|
+
- Element-wise `*` and `/` with another matrix of the same shape.
|
|
103
|
+
- `@` with another compatible matrix.
|
|
104
|
+
- `@` with a compatible vector.
|
|
105
|
+
- `T` for transpose, `norm()` for the Frobenius norm,
|
|
106
|
+
`get_row_elements()`, `get_col_elements()`, and `get_flatten_elements()`.
|
|
107
|
+
|
|
108
|
+
## Behavior notes
|
|
109
|
+
|
|
110
|
+
- Only plain Python `int` and `float` values are accepted.
|
|
111
|
+
- Division goes through `zero_aware_division()`. With the current default
|
|
112
|
+
configuration, dividing by zero returns `0` instead of raising an exception.
|
|
113
|
+
- The project is educational in scope and prioritizes clarity over performance.
|
|
114
|
+
|
|
115
|
+
## Running tests
|
|
116
|
+
|
|
117
|
+
From a repository checkout:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m unittest discover -s tests
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Repository examples
|
|
124
|
+
|
|
125
|
+
The repository includes a few small scripts in `examples/` that demonstrate the
|
|
126
|
+
API. From the repository root you can run them like this:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m examples.vector_plus_vector
|
|
130
|
+
python -m examples.vector_dot_product
|
|
131
|
+
python -m examples.matrix_dot_vector
|
|
132
|
+
python -m examples.vector_as_matrix
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Repository layout
|
|
136
|
+
|
|
137
|
+
- `linear_algebra_toolkit/objects.py` contains the `Vector` and `Matrix` implementations.
|
|
138
|
+
- `linear_algebra_toolkit/utils.py` contains small shared helpers.
|
|
139
|
+
- `tests/` contains the unit tests for the public behavior.
|
|
140
|
+
- `examples/` contains runnable usage examples.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
examples/matrix_dot_vector.py
|
|
6
|
+
examples/matrix_plus_matrix.py
|
|
7
|
+
examples/matrix_plus_vector.py
|
|
8
|
+
examples/vector_as_matrix.py
|
|
9
|
+
examples/vector_dot_matrix.py
|
|
10
|
+
examples/vector_dot_product.py
|
|
11
|
+
examples/vector_minus_vector.py
|
|
12
|
+
examples/vector_plus_matrix.py
|
|
13
|
+
examples/vector_plus_vector.py
|
|
14
|
+
linear_algebra_toolkit/__init__.py
|
|
15
|
+
linear_algebra_toolkit/objects.py
|
|
16
|
+
linear_algebra_toolkit/utils.py
|
|
17
|
+
linear_algebra_toolkit.egg-info/PKG-INFO
|
|
18
|
+
linear_algebra_toolkit.egg-info/SOURCES.txt
|
|
19
|
+
linear_algebra_toolkit.egg-info/dependency_links.txt
|
|
20
|
+
linear_algebra_toolkit.egg-info/top_level.txt
|
|
21
|
+
tests/test_objects.py
|
|
22
|
+
tests/test_utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linear_algebra_toolkit
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "linear-algebra-toolkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Educational pure-Python vector and matrix objects for linear algebra experiments."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Max B."}
|
|
15
|
+
]
|
|
16
|
+
keywords = ["linear algebra", "matrix", "vector", "education", "math"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Education",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Topic :: Education",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/maxboro/linear-algebra-toolkit"
|
|
31
|
+
Repository = "https://github.com/maxboro/linear-algebra-toolkit"
|
|
32
|
+
Issues = "https://github.com/maxboro/linear-algebra-toolkit/issues"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["linear_algebra_toolkit"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from linear_algebra_toolkit import Matrix, Vector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VectorTests(unittest.TestCase):
|
|
8
|
+
def test_vector_constructor_validates_input(self):
|
|
9
|
+
with self.assertRaises(TypeError):
|
|
10
|
+
Vector((1, 2, 3))
|
|
11
|
+
|
|
12
|
+
with self.assertRaises(ValueError):
|
|
13
|
+
Vector([])
|
|
14
|
+
|
|
15
|
+
with self.assertRaises(TypeError):
|
|
16
|
+
Vector([1, "x"])
|
|
17
|
+
|
|
18
|
+
def test_vector_shape_len_round_abs_and_matrix_views(self):
|
|
19
|
+
vector = Vector([1.234, -2.345, 3.456])
|
|
20
|
+
|
|
21
|
+
self.assertEqual(vector.shape, (3,))
|
|
22
|
+
self.assertEqual(len(vector), 3)
|
|
23
|
+
self.assertEqual(round(vector, 2), Vector([1.23, -2.35, 3.46]))
|
|
24
|
+
self.assertEqual(abs(vector), Vector([1.234, 2.345, 3.456]))
|
|
25
|
+
self.assertEqual(vector.as_row_matrix(), Matrix([[1.234, -2.345, 3.456]]))
|
|
26
|
+
self.assertEqual(vector.as_col_matrix(), Matrix([[1.234], [-2.345], [3.456]]))
|
|
27
|
+
|
|
28
|
+
def test_vector_add_and_subtract_vectors(self):
|
|
29
|
+
left = Vector([1, 2, 3])
|
|
30
|
+
right = Vector([4, 5, 6])
|
|
31
|
+
|
|
32
|
+
self.assertEqual(left + right, Vector([5, 7, 9]))
|
|
33
|
+
self.assertEqual(left - right, Vector([-3, -3, -3]))
|
|
34
|
+
|
|
35
|
+
def test_vector_add_and_subtract_row_and_column_matrices(self):
|
|
36
|
+
vector = Vector([1, 2, 3])
|
|
37
|
+
row_matrix = Matrix([[4, 5, 6]])
|
|
38
|
+
col_matrix = Matrix([[4], [5], [6]])
|
|
39
|
+
|
|
40
|
+
self.assertEqual(vector + row_matrix, Matrix([[5, 7, 9]]))
|
|
41
|
+
self.assertEqual(vector - row_matrix, Matrix([[-3, -3, -3]]))
|
|
42
|
+
self.assertEqual(vector + col_matrix, Matrix([[5], [7], [9]]))
|
|
43
|
+
self.assertEqual(vector - col_matrix, Matrix([[-3], [-3], [-3]]))
|
|
44
|
+
|
|
45
|
+
def test_vector_add_rejects_invalid_operands(self):
|
|
46
|
+
with self.assertRaises(RuntimeError):
|
|
47
|
+
Vector([1, 2]) + Vector([1])
|
|
48
|
+
|
|
49
|
+
with self.assertRaises(ValueError):
|
|
50
|
+
Vector([1, 2]) + Matrix([[1, 2], [3, 4]])
|
|
51
|
+
|
|
52
|
+
with self.assertRaises(TypeError):
|
|
53
|
+
Vector([1, 2]) + 1
|
|
54
|
+
|
|
55
|
+
def test_vector_multiply_and_divide(self):
|
|
56
|
+
left = Vector([2, 4, 6])
|
|
57
|
+
right = Vector([1, 2, 3])
|
|
58
|
+
divisor = Vector([2, 0, 3])
|
|
59
|
+
|
|
60
|
+
self.assertEqual(left * 2, Vector([4, 8, 12]))
|
|
61
|
+
self.assertEqual(3 * right, Vector([3, 6, 9]))
|
|
62
|
+
self.assertEqual(left * right, Vector([2, 8, 18]))
|
|
63
|
+
self.assertEqual(left / 2, Vector([1.0, 2.0, 3.0]))
|
|
64
|
+
self.assertEqual(left / divisor, Vector([1.0, 0, 2.0]))
|
|
65
|
+
|
|
66
|
+
with self.assertRaises(TypeError):
|
|
67
|
+
left * Matrix([[1, 2, 3]])
|
|
68
|
+
|
|
69
|
+
def test_vector_dot_products_and_matrix_product(self):
|
|
70
|
+
vector = Vector([5, 6])
|
|
71
|
+
other_vector = Vector([1, 0])
|
|
72
|
+
matrix = Matrix([[1, 2], [3, 4]])
|
|
73
|
+
|
|
74
|
+
self.assertEqual(vector @ other_vector, 5)
|
|
75
|
+
self.assertEqual(vector @ matrix, Vector([23, 34]))
|
|
76
|
+
|
|
77
|
+
def test_vector_matmul_rejects_incompatible_shapes(self):
|
|
78
|
+
with self.assertRaises(ValueError):
|
|
79
|
+
Vector([1, 2]) @ Matrix([[1, 2, 3]])
|
|
80
|
+
|
|
81
|
+
with self.assertRaises(TypeError):
|
|
82
|
+
Vector([1, 2]) @ 1
|
|
83
|
+
|
|
84
|
+
def test_vector_norms_and_normalized(self):
|
|
85
|
+
vector = Vector([3, 4])
|
|
86
|
+
normalized = vector.normalized
|
|
87
|
+
|
|
88
|
+
self.assertEqual(vector.norm(), 5.0)
|
|
89
|
+
self.assertEqual(vector.norm(mode="manhattan"), 7)
|
|
90
|
+
self.assertEqual(vector.norm(mode="max"), 4)
|
|
91
|
+
self.assertAlmostEqual(normalized.vector_elements[0], 0.6)
|
|
92
|
+
self.assertAlmostEqual(normalized.vector_elements[1], 0.8)
|
|
93
|
+
self.assertAlmostEqual(normalized.norm(), 1.0)
|
|
94
|
+
|
|
95
|
+
with self.assertRaises(ValueError):
|
|
96
|
+
vector.norm(mode="unknown")
|
|
97
|
+
|
|
98
|
+
with self.assertRaises(ValueError):
|
|
99
|
+
Vector([0, 0]).normalized
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MatrixTests(unittest.TestCase):
|
|
103
|
+
def test_matrix_constructor_validates_shape(self):
|
|
104
|
+
with self.assertRaises(TypeError):
|
|
105
|
+
Matrix((1, 2))
|
|
106
|
+
|
|
107
|
+
with self.assertRaises(ValueError):
|
|
108
|
+
Matrix([])
|
|
109
|
+
|
|
110
|
+
with self.assertRaises(TypeError):
|
|
111
|
+
Matrix([1, 2])
|
|
112
|
+
|
|
113
|
+
with self.assertRaises(ValueError):
|
|
114
|
+
Matrix([[1, 2], []])
|
|
115
|
+
|
|
116
|
+
with self.assertRaises(ValueError):
|
|
117
|
+
Matrix([[1, 2], [3]])
|
|
118
|
+
|
|
119
|
+
def test_matrix_shape_round_abs_transpose_and_flatten(self):
|
|
120
|
+
matrix = Matrix([[1.234, -2.345], [-3.456, 4.567]])
|
|
121
|
+
|
|
122
|
+
self.assertEqual(matrix.shape, (2, 2))
|
|
123
|
+
self.assertEqual(round(matrix, 2), Matrix([[1.23, -2.35], [-3.46, 4.57]]))
|
|
124
|
+
self.assertEqual(abs(matrix), Matrix([[1.234, 2.345], [3.456, 4.567]]))
|
|
125
|
+
self.assertEqual(matrix.T, Matrix([[1.234, -3.456], [-2.345, 4.567]]))
|
|
126
|
+
self.assertEqual(matrix.get_flatten_elements(), [1.234, -2.345, -3.456, 4.567])
|
|
127
|
+
|
|
128
|
+
def test_matrix_add_and_subtract_matrices(self):
|
|
129
|
+
left = Matrix([[1, 2], [3, 4]])
|
|
130
|
+
right = Matrix([[5, 6], [7, 8]])
|
|
131
|
+
|
|
132
|
+
self.assertEqual(left + right, Matrix([[6, 8], [10, 12]]))
|
|
133
|
+
self.assertEqual(left - right, Matrix([[-4, -4], [-4, -4]]))
|
|
134
|
+
|
|
135
|
+
def test_matrix_add_and_subtract_vectors(self):
|
|
136
|
+
row_matrix = Matrix([[1, 2, 3]])
|
|
137
|
+
col_matrix = Matrix([[1], [2], [3]])
|
|
138
|
+
vector = Vector([4, 5, 6])
|
|
139
|
+
|
|
140
|
+
self.assertEqual(row_matrix + vector, Matrix([[5, 7, 9]]))
|
|
141
|
+
self.assertEqual(row_matrix - vector, Matrix([[-3, -3, -3]]))
|
|
142
|
+
self.assertEqual(col_matrix + vector, Matrix([[5], [7], [9]]))
|
|
143
|
+
self.assertEqual(col_matrix - vector, Matrix([[-3], [-3], [-3]]))
|
|
144
|
+
|
|
145
|
+
def test_matrix_add_rejects_invalid_shapes(self):
|
|
146
|
+
with self.assertRaises(RuntimeError):
|
|
147
|
+
Matrix([[1, 2]]) + Matrix([[1], [2]])
|
|
148
|
+
|
|
149
|
+
with self.assertRaises(ValueError):
|
|
150
|
+
Matrix([[1, 2], [3, 4]]) + Vector([1, 2])
|
|
151
|
+
|
|
152
|
+
with self.assertRaises(TypeError):
|
|
153
|
+
Matrix([[1, 2]]) + 1
|
|
154
|
+
|
|
155
|
+
def test_matrix_multiply_and_divide(self):
|
|
156
|
+
left = Matrix([[2, 4], [6, 8]])
|
|
157
|
+
right = Matrix([[1, 2], [3, 4]])
|
|
158
|
+
divisor = Matrix([[2, 0], [3, 4]])
|
|
159
|
+
|
|
160
|
+
self.assertEqual(left * 2, Matrix([[4, 8], [12, 16]]))
|
|
161
|
+
self.assertEqual(3 * right, Matrix([[3, 6], [9, 12]]))
|
|
162
|
+
self.assertEqual(left * right, Matrix([[2, 8], [18, 32]]))
|
|
163
|
+
self.assertEqual(left / 2, Matrix([[1.0, 2.0], [3.0, 4.0]]))
|
|
164
|
+
self.assertEqual(left / divisor, Matrix([[1.0, 0], [2.0, 2.0]]))
|
|
165
|
+
|
|
166
|
+
with self.assertRaises(TypeError):
|
|
167
|
+
left / Vector([1, 2])
|
|
168
|
+
|
|
169
|
+
def test_matrix_products(self):
|
|
170
|
+
left = Matrix([[1, 2], [3, 4]])
|
|
171
|
+
right = Matrix([[5, 6], [7, 8]])
|
|
172
|
+
vector = Vector([5, 6])
|
|
173
|
+
|
|
174
|
+
self.assertEqual(left @ right, Matrix([[19, 22], [43, 50]]))
|
|
175
|
+
self.assertEqual(left @ vector, Vector([17, 39]))
|
|
176
|
+
|
|
177
|
+
def test_matrix_matmul_rejects_incompatible_shapes(self):
|
|
178
|
+
with self.assertRaises(RuntimeError):
|
|
179
|
+
Matrix([[1, 2]]) @ Matrix([[1, 2]])
|
|
180
|
+
|
|
181
|
+
with self.assertRaises(ValueError):
|
|
182
|
+
Matrix([[1, 2, 3]]) @ Vector([1, 2])
|
|
183
|
+
|
|
184
|
+
with self.assertRaises(TypeError):
|
|
185
|
+
Matrix([[1, 2]]) @ 1
|
|
186
|
+
|
|
187
|
+
def test_matrix_row_and_column_access_return_copies(self):
|
|
188
|
+
matrix = Matrix([[1, 2], [3, 4]])
|
|
189
|
+
|
|
190
|
+
row = matrix.get_row_elements(0)
|
|
191
|
+
column = matrix.get_col_elements(1)
|
|
192
|
+
|
|
193
|
+
row[0] = 99
|
|
194
|
+
column[0] = 88
|
|
195
|
+
|
|
196
|
+
self.assertEqual(matrix, Matrix([[1, 2], [3, 4]]))
|
|
197
|
+
|
|
198
|
+
def test_matrix_norm(self):
|
|
199
|
+
matrix = Matrix([[1, 2], [3, 4]])
|
|
200
|
+
|
|
201
|
+
self.assertAlmostEqual(matrix.norm(), math.sqrt(30))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
unittest.main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from linear_algebra_toolkit.utils import zero_aware_division, vector_dot_product
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UtilsTests(unittest.TestCase):
|
|
7
|
+
def test_zero_aware_division_returns_zero_for_zero_divisor(self):
|
|
8
|
+
self.assertEqual(zero_aware_division(5, 0), 0)
|
|
9
|
+
self.assertEqual(zero_aware_division(9, 3), 3)
|
|
10
|
+
|
|
11
|
+
def test_vector_dot_product(self):
|
|
12
|
+
self.assertEqual(vector_dot_product([1, 2, 3], [4, 5, 6]), 32)
|
|
13
|
+
|
|
14
|
+
with self.assertRaises(RuntimeError):
|
|
15
|
+
vector_dot_product([1, 2], [1])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
unittest.main()
|