Small size matrix handling module with a few linear algebra operations specifically for MicroPython (Python3)
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
LICENSE
README.md
ulinalg.py
ulinalg_tests.py
umatrix.py

README.md

Module umatrix, ulinalg

These are intended to be relatively small modules for use with MicroPython (Python3) which provide a minimal matrix class for representation, manipulation and a few linear algebra routines.

The matrix module is designed to offer close functional compatibility with 2-D Numpy arrays.

These routines are not designed to be particularly fast.

(eg. 3x3 matrix inversion takes about 350ms on a PyBoard.)

Files:

  • umatrix.py - matrix class.
  • ulinalg.py - supporting linear algebra routines (requires umatrix ).
  • ulinalg_tests.py - testing file to check most of the features.

Currently supported: (see following sections and Properties, Methods and Functions for descriptions)

Provided by umatrix

  • assignment
  • slicing (the third step argument is not supported)
  • matrix/scaler element wise arithmetic operations
  • Note: scaler OP matrix operations fail under MicroPython as reflected operations are not yet fully supported. Operations need to be arranged in a matrix OP scaler form. See the Implementation Notes section.
  • transpose
  • iteration support
  • sub-matrix assignment
  • reshaping

Provided by ulinalg

  • eye
  • ones
  • determinant and inverse
  • pseudo inverse (may need work)
  • dot product
  • cross product

Classes

umatrix.matrix

Matrix instantiation

Supported matrix element types default to bool and int. Support for float and complex are determined by the module upon importing and will depend on MicroPython compilation options and platform. All elements will be converted to the highest indexed type in the order of the above list (ie. if there is a float in the data, all elements will be converted to float). The result is held in umatrix.dtype. The kwarg dtype= may also be used to force the type.

A matrix can be constructed using a list of lists representation, where each list member is a row in the matrix.

import umatrix
X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])

Or by supplying a list of elements and the distance (strides) to move to get to the next column (cstride) and row (rstride).

X = umatrix.matrix([0,1,2,3,4,5,6,7,8,9,10,11], cstride=1, rstride=3)

Both the above will result in:

X = mat([[0 , 1 , 2 ],
         [3 , 4 , 5 ],
         [6 , 7 , 8 ],
         [9 , 10, 11]])

Using:

X = umatrix.matrix([0,1,2,3,4,5,6,7,8,9,10,11], cstride=1, rstride=3, dtype=float)

results in:

X = mat([[0.0 , 1.0 , 2.0 ],
         [3.0 , 4.0 , 5.0 ],
         [6.0 , 7.0 , 8.0 ],
         [9.0 , 10.0, 11.0]])

Matrix slicing

>>> import umatrix
>>> X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])
>>> X
mat([[0 ,1 ,2 ],
     [3 ,4 ,5 ],
     [6 ,7 ,8 ],
     [9 ,10,11]])
>>>
>>> X[1,3]        # Returns a single element as int, float, complex
>>> 6
>>> 
>>> X[1]          # Returns a row matrix (Numpy returns a vector)
mat([[3, 4, 5]])
>>>
>>> X[1,:]        # Returns a row matrix (Numpy returns a vector)
mat([[3, 4, 5]])
>>>
>>> X[:,1]        # Returns a column matrix (Numpy returns a vector)
mat([[1 ],
     [4 ],
     [7 ],
     [10]])
>>>
>>> X[:,1:3]      # Returns a submatrix of every row by columns 1 and 2
mat([[1 , 2 ],
     [4 , 5 ],
     [7 , 8 ],
     [10, 11]])
>>>
>>> X[1:3,2:4]    # Returns a submatrix
mat([[5, 6],
     [8, 9]])
>>>


Matrix assignment

Matrix elements can be assigned to using bool, int, float, complex, list or from a matrix. A list will assign elements in order from the source and wrap around if required.

>>>X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])
>>>X[1,:] = [20, 21, 22]
X
mat([[0 , 1 , 2 ],
     [20, 21, 22],
     [6 , 7 , 8 ],
     [9 , 10, 11]])
>>>X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])
>>>X[:,1] = [20, 21, 22]
X
mat([[0 , 20, 2 ],
     [3 , 21, 5 ],
     [6 , 22, 8 ],
     [9 , 20, 11]])  # wraps around to the first element of the source list
>>>X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])
>>>X[1:3,1:3] = [20, 21, 22, 23]
X
mat([[0 , 1 , 2 ],
     [3 , 20, 21],
     [6 , 22, 23],
     [9 , 10, 11]])

Iteration

Iterating over a matrix will return the rows as a list of 1xn matrices (Numpy returns a list of vectors).

>>>X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])
>>>[i for i in X]
[mat([[0, 1, 2]]), mat([[3, 4, 5]]), mat([[6, 7, 8]]), mat([[9 , 10, 11]])]

Iterating over a slice of a matrix will return a list of elements.

>>>X = umatrix.matrix([[0,1,2],[3,4,5],[6,7,8],[9,10,11]])
[i for i in X[1,:]]
[3, 4, 5]

Implementation Notes

Differences from Numpy Arrays

  • Slices are always a view in Numpy, in umatrix they are currently not a view
  • Scaler as left hand side argument for [+,-,*,,\] operations are not supported in umatrix (see below) but a matrix as left hand side is.
  • Single row/col slices are 1-D arrays in Numpy and a 1xn (row) or nx1 (column) matrix in umatrix
  • Numpy has a 0-d array, numpy.array(2) a special vector that acts like a scaler.
  • umatrix doesn't support NaN, Inf, -Inf

Types

The umatrix module attempts to determine the supported types and floating point epsilon if float is supported.

The results are held in umatrix.stypes and umatrix.flt_eps respectively.

The variable umatrix.dtype holds the default type used by some ulinalg routines (float if supported) .

For example flt_eps, stypes under a few different platforms:

#    PyBoard         = 1.19E-7 , [<class 'bool'>, <class 'int'>, <class 'float'>, <class 'complex'>]
#    WiPy            = 1       , [<class 'bool'>, <class 'int'>]
#    Linux (uPy)     = 1.19E-7 , [<class 'bool'>, <class 'int'>, <class 'float'>, <class 'complex'>]
#    Linux (CPython) = 2.22E-16, [<class 'bool'>, <class 'int'>, <class 'float'>, <class 'complex'>]

Notes:

  • uPy under Linux forces flt_eps = 1.19E-7 since it appears that the underlying math functions use doubles but uPy operates with single precision.
  • flt_eps is kind of irrelevant when all the work is done on one platform but the ulinalg_test file uses it to determine matrix equality.

Matrix +,-,*,\,\\ Scaler operations

Matrices used as the LH argument of a scaler operation will work for element wise operation. Using a scaler as the RH argument does not work as reflected operations are not yet supported by MicroPython.

Negation of a matrix does work.

For example:

2+X     # does not work
X+2     # matrix plus element-wise addition works

2-X     # does not work
-X+2    # works

2*X     # does not work
X*2     # works

Scaler / matrix can be accomplished using the reciprocal(n=1) method.

2.0/X              # does not work
X.reciprocal()*2.0 # does work (1/x * 2.0)

The reason seems to be that the MicroPython int class __add__ method (for example) does not raise NotImplementedError and therefore the umatrix __radd__ method is not invoked.


Matrix equality

In MicroPython X == Y returns True if all elements of X and Y are equal and the they have the same shape.

The following functions are available:

  • umatrix.matrix_equal(X, Y) - boolean indicating same data and shape.
  • umatrix.matrix_equv(X, Y, tol=0) - boolean indication same data (within tol) and broadcastable.
  • umatrix.matrix.isclose(X, Y, rtol=1.0E-5, atol=flt_eps) - boolean matrix indicating element wise equality (within tol).

Float and complex default to determining equality within flt_eps.

In Numpy X == Y returns a boolean matrix indicating element equality. To get a similar result to Numpy, use umatrix.matrix_isclose(X, Y).


Properties of umatrix.matrix

shape

Returns the shape as a tuple (m, n).

shape = (p, q)

In place shape change (not a copy)

is_square

Returns True if a square matrix

T

Convenience property to return a transposed view


Methods of umatrix.matrix

copy()

Returns a copy of the matrix

size(axis=0)

Returns:

axis=0 size (n*m) for Numpy compatibility

axis=1 rows

axis=2 columns

reshape(m, n)

Returns a copy of the matrix with a the shape (m, n)

transpose()

Returns a view of the matrix transpose

reciprocal(n=1)

Returns a matrix with element wise reciprocals by default (or n/element). For use in scaler division. See Implementation Notes.

apply(func, *args, **kwargs)

Call a scalar function on each element, returns a new matrix passes *args and **kwargs to func unmodified note: this is not useful for matrix-matrix operations

e.g.

   y = x.apply(math.sin)
   y = x.apply(lambda a,b: a>b, 5) # equivalent to y = x > 5
   y = x.apply(operators.gt, 5)    # equivalent to y = x > 5 (not in MicroPython)

Functions provided by umatrix module

isclose(X, Y, rtol=1.0E-5, atol=flt_eps)

Returns True if matrices X and Y have the same shape and the elements are within tol. tol defaults to umatrix.flt_eps for use with int and to flt_eps for float and complex.

Ref. numpy.isclose()

matrix_equal(X, Y, tol=0)

Returns a boolean matrix indicating element equality. X and Y must be the same shape. Similar to Numpys ==

matrix_equiv(X, Y)

Returns a boolean indicating if X and Y share the same data and are broadcastable


Functions provided by ulinalg module

eps(a=1)

Returns the closest floating point number within machine tolerance considered to be different from 'a'.

Ref. numpy.spacing(), MATLAB/Octave eps() functions

zeros(m, n, dtype=umatrix.ddtype)

Returns a m x n matrix filled with zeros.

ones(m, n, dtype=umatrix.ddtype)

Returns a m x n matrix filled with ones.

eye(m, dtype=umatrix.ddtype)

Returns a m x m matrix with the diagonal of ones.

det_inv(X)

Returns the determinant and inverse (d, i) of X if it exists. Uses Gaussian Elimination to get an upper triangular matrix.

Returns (0, []) if the matrix is singular.

Numpy.linalg provides separate functions for det and inv.

pinv(X)

Returns the results of the pseudo inverse operation of X.

Not sure how robust this is but it works for at least one example.

dot(X, Y)

The dot product operation.

cross(X, Y, axis=1)

The cross product operation for 2x2 and 3x3 matrices.

axis = 1 (default) is for Numpy compatibility.

axis = 0 is for MATLAB, Octave, SciLab compatibility.