umatrix
was written mainly with speed and ease-of-use in mind. It aims to be a simple solution to matrix arithmetic needs in Micropython. It implements basic matrix operations (addition, subtraction, multiplication) as well as determinant (shortened to det
), inverse
, trace
, transpose
, copy
, and other functions. The matrix class supports int
, float
, and complex
coefficients, as well as numpy
-like matrix slicing.
>>> from umatrix import *
>>> A = matrix([1, 2, 3], [4, 5, 6], [7, 8, 9])
>>> A
matrix( [1, 2, 3],
[4, 5, 6],
[7, 8, 9] )
>>> M = matrix([12, 23, 31], [40, 50, 60], [71, 87, 98])
>>> print(M)
[12, 23, 31,
40, 50, 60,
71, 87, 98]
>>> N = matrix([12.516j, 6.345, 7+.171j], are_rows=False)
>>> N
matrix( [ 12.516j],
[ 6.345],
[(7+0.171j)] )
>>> A.order
3
>>> A.is_square
True
>>> N.is_square
False
>>> N.shape
(3, 1)
>>> N.rows
[[12.516j], [6.345], [(7+0.171j)]]
>>> N.cols
[[12.516j, 6.345, (7+0.171j)]]
>>> A+M
matrix( [ 13, 25, 34],
[ 44, 55, 66],
[ 78, 95, 107] )
>>> M-A
matrix( [11, 21, 28],
[36, 45, 54],
[64, 79, 89] )
>>> A*M
matrix( [ 305, 384, 445],
[ 674, 864, 1012],
[1043, 1344, 1579] )
>>> M*A
matrix( [ 321, 387, 453],
[ 660, 810, 960],
[1105, 1361, 1617] )
>>> A**2
matrix( [ 30, 36, 42],
[ 66, 81, 96],
[102, 126, 150] )
>>> M*2
matrix( [ 24, 46, 62],
[ 80, 100, 120],
[142, 174, 196] )
>>> 2*M
matrix( [ 24, 46, 62],
[ 80, 100, 120],
[142, 174, 196] )
Supports <= 4x4 matrices.
>>> A.det
0
>>> M.det
1810
>>> abs(M)
1810
>>> N.det
[Traceback]
AssertionError: Matrix must be square.
Supports <= 4x4 matrices.
>>> A.inverse
[Traceback]
AssertionError: Matrix is singular.
>>> M.inverse
matrix( [ -0.1767956, 0.2447514, -0.09392265],
[ 0.1878453, -0.5662984, 0.2872928],
[-0.03867403, 0.3254144, -0.1767956] )
Note that calling round
on a matrix will not work as Micropython has not implemented the __round__
magic method, so rounding a matrix requires you call its defined round
method with the accustomed decimal places argument. The round
method supports complex
coefficients. The boolean argument inplace
set to False
by default makes the changes "in place" when set to True
.
>>> (M*M.inverse).round(5)
matrix( [ 1.0, 0.0, -0.0],
[ 0.0, 1.0, 0.0],
[ 0.0, -0.0, 1.0] )
>>> N
matrix( [12.516j],
[6.345],
[(7+0.171j)] )
>>> N.round(2, True)
>>> N
matrix( [12.52j],
[6.34],
[(7+0.17j)] )
>>> B = M.copy()
>>> M
matrix( [12, 23, 31],
[40, 50, 60],
[71, 87, 98] )
>>> B[0] = [2222,2222,2222]
>>> B
matrix( [2222, 2222, 2222],
[ 40, 50, 60],
[ 71, 87, 98] )
>>> M
matrix( [12, 23, 31],
[40, 50, 60],
[71, 87, 98] )
>>> M.det == B.det
False
>>> A
matrix( [1, 2, 3],
[4, 5, 6],
[7, 8, 9] )
>>> A.transpose
matrix( [1, 4, 7],
[2, 5, 8],
[3, 6, 9] )
>>> N
matrix( [12.516j],
[6.345],
[(7+0.171j)] )
>>> N.transpose
matrix( [12.516j, 6.345, (7+0.171j)] )
>>> A
matrix( [1, 2, 3],
[4, 5, 6],
[7, 8, 9] )
>>> A.trace
15
>>> M
matrix( [12, 23, 31],
[40, 50, 60],
[71, 87, 98] )
>>> M.trace
160
is_eigenvalue
takes the value to check as its single argument. is_eigenvector
takes the vector and value to check respectively: the vector can be given in tuple/list form or in matrix form.
>>> C = matrix([1, 2], [2, 1])
>>> C
matrix( [1, 2],
[2, 1] )
>>> C.is_eigenvalue(2.431)
False
>>> C.is_eigenvalue(3)
True
>>> C.is_eigenvalue(-1)
True
>>> C.is_eigenvector((1, -1), -1)
True
>>> C.is_eigenvector(matrix([1, 1], are_rows=False), 3)
True
>>> A == M
False
>>> N != A
True
>>> A == A.copy()
True
This method takes a function and the boolean inplace
as its arguments. It applies that function to the matrix's coefficients and either returns a new matrix with the return values or overwrites the initial matrix's coefficients. By default, inplace = False
.
>>> A
matrix( [1, 2, 3],
[4, 5, 6],
[7, 8, 9] )
>>> from math import log
>>> A.apply(log).round(2)
matrix( [ 0.0, 0.69, 1.1],
[1.39, 1.61, 1.79],
[1.95, 2.08, 2.2] )
>>> A.apply(lambda x: x if x > 5 else 0, inplace=True)
>>> A
matrix( [0, 0, 0],
[0, 0, 6],
[7, 8, 9] )
Referencing a matrix with a tuple
of either two slice
s or a slice
and an int
returns a new matrix.
You can also modify matrix coefficients by assigning values to a matrix slice
.
Note that for changing the value of a specific coefficient, you should use matrix[idx1][idx2] = new_val
and not a slice
assignment.
>>> Z = matrix([1,2,3,4],[5,6,7,8],[8,9,0,1],[2,3,4,5],[6,7,8,9])
>>> Z
matrix( [1, 2, 3, 4],
[5, 6, 7, 8],
[8, 9, 0, 1],
[2, 3, 4, 5],
[6, 7, 8, 9] )
>>> Z[:, 3]
matrix( [4],
[8],
[1],
[5],
[9] )
>>> Z[0, ::2]
matrix( [1, 3] )
>>> Z[1:4, 1:]
matrix( [6, 7, 8],
[9, 0, 1],
[3, 4, 5] )
>>> Z[::2, 2]
matrix( [3],
[0],
[8] )
>>> Z[::2, 2] = 1111, 1111, 1111
>>> Z
matrix( [ 1, 2, 1111, 4],
[ 5, 6, 7, 8],
[ 8, 9, 1111, 1],
[ 2, 3, 4, 5],
[ 6, 7, 1111, 9] )
>>> Z[:, 2:]
matrix( [1111, 4],
[ 7, 8],
[1111, 1],
[ 4, 5],
[1111, 9] )
>>> Z[:, 2:] = [[0]*2]*5
>>> Z
matrix( [1, 2, 0, 0],
[5, 6, 0, 0],
[8, 9, 0, 0],
[2, 3, 0, 0],
[6, 7, 0, 0] )
>>> Z[1, ::2]
matrix( [5, 0] )
>>> Z[1, ::2] = [5555, 5555]
>>> Z
matrix( [ 1, 2, 0, 0],
[5555, 6, 5555, 0],
[ 8, 9, 0, 0],
[ 2, 3, 0, 0],
[ 6, 7, 0, 0] )
>>> Z[4][1] = 9999
>>> Z
matrix( [ 1, 2, 0, 0],
[5555, 6, 5555, 0],
[ 8, 9, 0, 0],
[ 2, 3, 0, 0],
[ 6, 9999, 0, 0] )
Note that for fill
, zeros
, and ones
, if the number of columns is not supplied as a final argument a square matrix is returned.
eye
always returns a square matrix.
eye
: returns the identity matrix.
>>> eye(4)
matrix( [1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1] )
fill
: returns a matrix filled with the coefficient of your choice.
>>> fill(25, 3, 6)
matrix( [25, 25, 25, 25, 25, 25],
[25, 25, 25, 25, 25, 25],
[25, 25, 25, 25, 25, 25] )
>>> fill(3.14, A.order)
matrix( [3.14, 3.14, 3.14],
[3.14, 3.14, 3.14],
[3.14, 3.14, 3.14] )
zeros
: returns a matrix filled with zeros.
>>> zeros(5,4)
matrix( [0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0] )
ones
: returns a matrix filled with ones.
>>> ones(4,5)
matrix( [1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1] )
For both the Pyboard v1.1 and Pyboard v1.0 lite tests, no SD card was used.
There were only 3 files onboard the flash storage: boot.py
(factory state), main.py
(as described below), and umatrix.py
.
main.py
consisted of the following lines:
from umatrix import *
from utime import ticks_us
def t(func, n):
# returns the average time for n executions of func in milliseconds
start = ticks_us()
for i in range(n):
_ = func()
end = ticks_us()
return ((end-start)/1000)/n
T = matrix([12, 62], [98, 21]) # 2x2
U = T**3 # 2x2 large coefficients
V = matrix([57, 45, 67], [77, 40, 93], [12, 76, 34]) # 3x3
W = V**3 #3x3 large coefficients
X = matrix([10, 49, 36, 54], [88, 61, 53, 20], [31, 42, 53, 64], [9, 75, 60, 75]) # 4x4
Y = X**3 # 4x4 large coefficients
Visualisation:
>>> T
matrix( [12, 62],
[98, 21] )
>>> U
matrix( [275148, 428606],
[677474, 337365] )
>>> V
matrix( [57, 45, 67],
[77, 40, 93],
[12, 76, 34] )
>>> W
matrix( [1280099, 1498022, 1732795],
[1568078, 1786761, 2112958],
[ 978772, 1245168, 1345452] )
>>> X
matrix( [10, 49, 36, 54],
[88, 61, 53, 20],
[31, 42, 53, 64],
[ 9, 75, 60, 75] )
>>> Y
matrix( [1177869, 1777147, 1597682, 1614846],
[1535988, 2364798, 2117353, 2152054],
[1445741, 2205124, 1984654, 2000664],
[1724826, 2616789, 2351580, 2386851] )
The timing function t
was called in the REPL with n = 5000
. The reported result is the slowest of 3 tests i.e. 3 executions of t(func, 5000)
.
A reminder that the results are in milliseconds.
umatrix
v1.1
Matrix Size | Small Coefficients | Large Coefficients |
---|---|---|
2x2 | 0.6740602 | 0.7213964 |
3x3 | 1.072958 | 1.573 |
4x4 | 1.734226 | 4.559215 |
umatrix
v1.1
Matrix Size | Small Coefficients | Large Coefficients |
---|---|---|
2x2 | 1.137326 | 1.217149 |
3x3 | 1.831495 | 2.671786 |
4x4 | 2.979418 | 7.701244 |
There is a clear increase in execution time for either tests as the matrix size and coefficients get larger.