320 lines
8.6 KiB
ReStructuredText
320 lines
8.6 KiB
ReStructuredText
|
|
Linalg
|
|
======
|
|
|
|
Functions in the ``linalg`` module can be called by prepending them by
|
|
``numpy.linalg.``. The module defines the following seven functions:
|
|
|
|
1. `numpy.linalg.cholesky <#cholesky>`__
|
|
2. `numpy.linalg.det <#det>`__
|
|
3. `numpy.linalg.dot <#dot>`__
|
|
4. `numpy.linalg.eig <#eig>`__
|
|
5. `numpy.linalg.inv <#inv>`__
|
|
6. `numpy.linalg.norm <#norm>`__
|
|
7. `numpy.linalg.trace <#trace>`__
|
|
|
|
cholesky
|
|
--------
|
|
|
|
``numpy``:
|
|
https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.linalg.cholesky.html
|
|
|
|
The function of the Cholesky decomposition takes a positive definite,
|
|
symmetric square matrix as its single argument, and returns the *square
|
|
root matrix* in the lower triangular form. If the input argument does
|
|
not fulfill the positivity or symmetry condition, a ``ValueError`` is
|
|
raised.
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
a = np.array([[25, 15, -5], [15, 18, 0], [-5, 0, 11]])
|
|
print('a: ', a)
|
|
print('\n' + '='*20 + '\nCholesky decomposition\n', np.linalg.cholesky(a))
|
|
|
|
.. parsed-literal::
|
|
|
|
a: array([[25.0, 15.0, -5.0],
|
|
[15.0, 18.0, 0.0],
|
|
[-5.0, 0.0, 11.0]], dtype=float)
|
|
|
|
====================
|
|
Cholesky decomposition
|
|
array([[5.0, 0.0, 0.0],
|
|
[3.0, 3.0, 0.0],
|
|
[-1.0, 1.0, 3.0]], dtype=float)
|
|
|
|
|
|
|
|
|
|
det
|
|
---
|
|
|
|
``numpy``:
|
|
https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.det.html
|
|
|
|
The ``det`` function takes a square matrix as its single argument, and
|
|
calculates the determinant. The calculation is based on successive
|
|
elimination of the matrix elements, and the return value is a float,
|
|
even if the input array was of integer type.
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
a = np.array([[1, 2], [3, 4]], dtype=np.uint8)
|
|
print(np.linalg.det(a))
|
|
|
|
.. parsed-literal::
|
|
|
|
-2.0
|
|
|
|
|
|
|
|
Benchmark
|
|
~~~~~~~~~
|
|
|
|
Since the routine for calculating the determinant is pretty much the
|
|
same as for finding the `inverse of a matrix <#inv>`__, the execution
|
|
times are similar:
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
@timeit
|
|
def matrix_det(m):
|
|
return np.linalg.inv(m)
|
|
|
|
m = np.array([[1, 2, 3, 4, 5, 6, 7, 8], [0, 5, 6, 4, 5, 6, 4, 5],
|
|
[0, 0, 9, 7, 8, 9, 7, 8], [0, 0, 0, 10, 11, 12, 11, 12],
|
|
[0, 0, 0, 0, 4, 6, 7, 8], [0, 0, 0, 0, 0, 5, 6, 7],
|
|
[0, 0, 0, 0, 0, 0, 7, 6], [0, 0, 0, 0, 0, 0, 0, 2]])
|
|
|
|
matrix_det(m)
|
|
|
|
.. parsed-literal::
|
|
|
|
execution time: 294 us
|
|
|
|
|
|
|
|
eig
|
|
---
|
|
|
|
``numpy``:
|
|
https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.eig.html
|
|
|
|
The ``eig`` function calculates the eigenvalues and the eigenvectors of
|
|
a real, symmetric square matrix. If the matrix is not symmetric, a
|
|
``ValueError`` will be raised. The function takes a single argument, and
|
|
returns a tuple with the eigenvalues, and eigenvectors. With the help of
|
|
the eigenvectors, amongst other things, you can implement sophisticated
|
|
stabilisation routines for robots.
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
a = np.array([[1, 2, 1, 4], [2, 5, 3, 5], [1, 3, 6, 1], [4, 5, 1, 7]], dtype=np.uint8)
|
|
x, y = np.linalg.eig(a)
|
|
print('eigenvectors of a:\n', y)
|
|
print('\neigenvalues of a:\n', x)
|
|
|
|
.. parsed-literal::
|
|
|
|
eigenvectors of a:
|
|
array([[0.8151560042509081, -0.4499411232970823, -0.1644660242574522, 0.3256141906686505],
|
|
[0.2211334179893007, 0.7846992598235538, 0.08372081379922657, 0.5730077734355189],
|
|
[-0.1340114162071679, -0.3100776411558949, 0.8742786816656, 0.3486109343758527],
|
|
[-0.5183258053659028, -0.292663481927148, -0.4489749870391468, 0.6664142156731531]], dtype=float)
|
|
|
|
eigenvalues of a:
|
|
array([-1.165288365404889, 0.8029365530314914, 5.585625756072663, 13.77672605630074], dtype=float)
|
|
|
|
|
|
|
|
|
|
The same matrix diagonalised with ``numpy`` yields:
|
|
|
|
.. code::
|
|
|
|
# code to be run in CPython
|
|
|
|
a = array([[1, 2, 1, 4], [2, 5, 3, 5], [1, 3, 6, 1], [4, 5, 1, 7]], dtype=np.uint8)
|
|
x, y = eig(a)
|
|
print('eigenvectors of a:\n', y)
|
|
print('\neigenvalues of a:\n', x)
|
|
|
|
.. parsed-literal::
|
|
|
|
eigenvectors of a:
|
|
[[ 0.32561419 0.815156 0.44994112 -0.16446602]
|
|
[ 0.57300777 0.22113342 -0.78469926 0.08372081]
|
|
[ 0.34861093 -0.13401142 0.31007764 0.87427868]
|
|
[ 0.66641421 -0.51832581 0.29266348 -0.44897499]]
|
|
|
|
eigenvalues of a:
|
|
[13.77672606 -1.16528837 0.80293655 5.58562576]
|
|
|
|
|
|
When comparing results, we should keep two things in mind:
|
|
|
|
1. the eigenvalues and eigenvectors are not necessarily sorted in the
|
|
same way
|
|
2. an eigenvector can be multiplied by an arbitrary non-zero scalar, and
|
|
it is still an eigenvector with the same eigenvalue. This is why all
|
|
signs of the eigenvector belonging to 5.58, and 0.80 are flipped in
|
|
``ulab`` with respect to ``numpy``. This difference, however, is of
|
|
absolutely no consequence.
|
|
|
|
Computation expenses
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Since the function is based on `Givens
|
|
rotations <https://en.wikipedia.org/wiki/Givens_rotation>`__ and runs
|
|
till convergence is achieved, or till the maximum number of allowed
|
|
rotations is exhausted, there is no universal estimate for the time
|
|
required to find the eigenvalues. However, an order of magnitude can, at
|
|
least, be guessed based on the measurement below:
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
@timeit
|
|
def matrix_eig(a):
|
|
return np.linalg.eig(a)
|
|
|
|
a = np.array([[1, 2, 1, 4], [2, 5, 3, 5], [1, 3, 6, 1], [4, 5, 1, 7]], dtype=np.uint8)
|
|
|
|
matrix_eig(a)
|
|
|
|
.. parsed-literal::
|
|
|
|
execution time: 111 us
|
|
|
|
|
|
|
|
inv
|
|
---
|
|
|
|
``numpy``:
|
|
https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.linalg.inv.html
|
|
|
|
A square matrix, provided that it is not singular, can be inverted by
|
|
calling the ``inv`` function that takes a single argument. The inversion
|
|
is based on successive elimination of elements in the lower left
|
|
triangle, and raises a ``ValueError`` exception, if the matrix turns out
|
|
to be singular (i.e., one of the diagonal entries is zero).
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
m = np.array([[1, 2, 3, 4], [4, 5, 6, 4], [7, 8.6, 9, 4], [3, 4, 5, 6]])
|
|
|
|
print(np.linalg.inv(m))
|
|
|
|
.. parsed-literal::
|
|
|
|
array([[-2.166666666666667, 1.500000000000001, -0.8333333333333337, 1.0],
|
|
[1.666666666666667, -3.333333333333335, 1.666666666666668, -0.0],
|
|
[0.1666666666666666, 2.166666666666668, -0.8333333333333337, -1.0],
|
|
[-0.1666666666666667, -0.3333333333333333, 0.0, 0.5]], dtype=float64)
|
|
|
|
|
|
|
|
|
|
Computation expenses
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Note that the cost of inverting a matrix is approximately twice as many
|
|
floats (RAM), as the number of entries in the original matrix, and
|
|
approximately as many operations, as the number of entries. Here are a
|
|
couple of numbers:
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
@timeit
|
|
def invert_matrix(m):
|
|
return np.linalg.inv(m)
|
|
|
|
m = np.array([[1, 2,], [4, 5]])
|
|
print('2 by 2 matrix:')
|
|
invert_matrix(m)
|
|
|
|
m = np.array([[1, 2, 3, 4], [4, 5, 6, 4], [7, 8.6, 9, 4], [3, 4, 5, 6]])
|
|
print('\n4 by 4 matrix:')
|
|
invert_matrix(m)
|
|
|
|
m = np.array([[1, 2, 3, 4, 5, 6, 7, 8], [0, 5, 6, 4, 5, 6, 4, 5],
|
|
[0, 0, 9, 7, 8, 9, 7, 8], [0, 0, 0, 10, 11, 12, 11, 12],
|
|
[0, 0, 0, 0, 4, 6, 7, 8], [0, 0, 0, 0, 0, 5, 6, 7],
|
|
[0, 0, 0, 0, 0, 0, 7, 6], [0, 0, 0, 0, 0, 0, 0, 2]])
|
|
print('\n8 by 8 matrix:')
|
|
invert_matrix(m)
|
|
|
|
.. parsed-literal::
|
|
|
|
2 by 2 matrix:
|
|
execution time: 65 us
|
|
|
|
4 by 4 matrix:
|
|
execution time: 105 us
|
|
|
|
8 by 8 matrix:
|
|
execution time: 299 us
|
|
|
|
|
|
|
|
The above-mentioned scaling is not obeyed strictly. The reason for the
|
|
discrepancy is that the function call is still the same for all three
|
|
cases: the input must be inspected, the output array must be created,
|
|
and so on.
|
|
|
|
norm
|
|
----
|
|
|
|
``numpy``:
|
|
https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html
|
|
|
|
The function takes a vector or matrix without options, and returns its
|
|
2-norm, i.e., the square root of the sum of the square of the elements.
|
|
|
|
.. code::
|
|
|
|
# code to be run in micropython
|
|
|
|
from ulab import numpy as np
|
|
|
|
a = np.array([1, 2, 3, 4, 5])
|
|
b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
|
|
print('norm of a:', np.linalg.norm(a))
|
|
print('norm of b:', np.linalg.norm(b))
|
|
|
|
.. parsed-literal::
|
|
|
|
norm of a: 7.416198487095663
|
|
norm of b: 16.88194301613414
|
|
|
|
|
|
|