aboutsummaryrefslogtreecommitdiff
path: root/circuitpython/extmod/ulab/docs
diff options
context:
space:
mode:
authorRaghuram Subramani <raghus2247@gmail.com>2022-06-19 19:47:51 +0530
committerRaghuram Subramani <raghus2247@gmail.com>2022-06-19 19:47:51 +0530
commit4fd287655a72b9aea14cdac715ad5b90ed082ed2 (patch)
tree65d393bc0e699dd12d05b29ba568e04cea666207 /circuitpython/extmod/ulab/docs
parent0150f70ce9c39e9e6dd878766c0620c85e47bed0 (diff)
add circuitpython code
Diffstat (limited to 'circuitpython/extmod/ulab/docs')
-rw-r--r--circuitpython/extmod/ulab/docs/manual/Makefile24
-rw-r--r--circuitpython/extmod/ulab/docs/manual/make.bat35
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/conf.py112
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/index.rst38
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/numpy-fft.rst197
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/numpy-functions.rst1664
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/numpy-linalg.rst386
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/numpy-universal.rst487
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/scipy-linalg.rst151
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/scipy-optimize.rst173
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/scipy-signal.rst137
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/scipy-special.rst44
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/ulab-intro.rst624
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst2607
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/ulab-programming.rst911
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/ulab-tricks.rst268
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/ulab-utils.rst143
-rw-r--r--circuitpython/extmod/ulab/docs/numpy-fft.ipynb546
-rw-r--r--circuitpython/extmod/ulab/docs/numpy-functions.ipynb2393
-rw-r--r--circuitpython/extmod/ulab/docs/numpy-linalg.ipynb811
-rw-r--r--circuitpython/extmod/ulab/docs/numpy-universal.ipynb869
-rw-r--r--circuitpython/extmod/ulab/docs/scipy-linalg.ipynb474
-rw-r--r--circuitpython/extmod/ulab/docs/scipy-optimize.ipynb515
-rw-r--r--circuitpython/extmod/ulab/docs/scipy-signal.ipynb482
-rw-r--r--circuitpython/extmod/ulab/docs/scipy-special.ipynb344
-rw-r--r--circuitpython/extmod/ulab/docs/templates/manual.tpl113
-rw-r--r--circuitpython/extmod/ulab/docs/templates/rst.tpl144
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-approx.ipynb613
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-change-log.md957
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-compare.ipynb467
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-convert.ipynb507
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-intro.ipynb897
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-ndarray.ipynb3754
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-numerical.ipynb1160
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-poly.ipynb454
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-programming.ipynb798
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-tricks.ipynb582
-rw-r--r--circuitpython/extmod/ulab/docs/ulab-utils.ipynb471
38 files changed, 25352 insertions, 0 deletions
diff --git a/circuitpython/extmod/ulab/docs/manual/Makefile b/circuitpython/extmod/ulab/docs/manual/Makefile
new file mode 100644
index 0000000..a97f725
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/Makefile
@@ -0,0 +1,24 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+clean:
+ rm -rf "$(BUILDDIR)"
+
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/circuitpython/extmod/ulab/docs/manual/make.bat b/circuitpython/extmod/ulab/docs/manual/make.bat
new file mode 100644
index 0000000..6247f7e
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/circuitpython/extmod/ulab/docs/manual/source/conf.py b/circuitpython/extmod/ulab/docs/manual/source/conf.py
new file mode 100644
index 0000000..5c7b7dc
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/conf.py
@@ -0,0 +1,112 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+#import sphinx_rtd_theme
+
+from sphinx.transforms import SphinxTransform
+from docutils import nodes
+from sphinx import addnodes
+
+# -- Project information -----------------------------------------------------
+
+project = 'The ulab book'
+copyright = '2019-2022, Zoltán Vörös and contributors'
+author = 'Zoltán Vörös'
+
+# The full version, including alpha/beta/rc tags
+release = '4.0.0'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+latex_maketitle = r'''
+\begin{titlepage}
+\begin{flushright}
+\Huge\textbf{The $\mu$lab book}
+\vskip 0.5em
+\LARGE
+\textbf{Release %s}
+\vskip 5em
+\huge\textbf{Zoltán Vörös}
+\end{flushright}
+\begin{flushright}
+\LARGE
+\vskip 2em
+with contributions by
+\vskip 2em
+\textbf{Roberto Colistete Jr.}
+\vskip 0.2em
+\textbf{Jeff Epler}
+\vskip 0.2em
+\textbf{Taku Fukada}
+\vskip 0.2em
+\textbf{Diego Elio Pettenò}
+\vskip 0.2em
+\textbf{Scott Shawcroft}
+\vskip 5em
+\today
+\end{flushright}
+\end{titlepage}
+'''%release
+
+latex_elements = {
+ 'maketitle': latex_maketitle
+}
+
+
+master_doc = 'index'
+
+author=u'Zoltán Vörös'
+copyright=author
+language='en'
+
+latex_documents = [
+(master_doc, 'the-ulab-book.tex', 'The $\mu$lab book',
+'Zoltán Vörös', 'manual'),
+]
+
+# Read the docs theme
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+if not on_rtd:
+ try:
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.']
+ except ImportError:
+ html_theme = 'default'
+ html_theme_path = ['.']
+else:
+ html_theme_path = ['.']
diff --git a/circuitpython/extmod/ulab/docs/manual/source/index.rst b/circuitpython/extmod/ulab/docs/manual/source/index.rst
new file mode 100644
index 0000000..1bae7a3
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/index.rst
@@ -0,0 +1,38 @@
+
+.. ulab-manual documentation master file, created by
+ sphinx-quickstart on Sat Oct 19 12:48:00 2019.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to the ulab book!
+=======================================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Introduction
+
+ ulab-intro
+
+.. toctree::
+ :maxdepth: 2
+ :caption: User's guide:
+
+ ulab-ndarray
+ numpy-functions
+ numpy-universal
+ numpy-fft
+ numpy-linalg
+ scipy-linalg
+ scipy-optimize
+ scipy-signal
+ scipy-special
+ ulab-utils
+ ulab-tricks
+ ulab-programming
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/circuitpython/extmod/ulab/docs/manual/source/numpy-fft.rst b/circuitpython/extmod/ulab/docs/manual/source/numpy-fft.rst
new file mode 100644
index 0000000..7da9b60
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/numpy-fft.rst
@@ -0,0 +1,197 @@
+
+numpy.fft
+=========
+
+Functions related to Fourier transforms can be called by prepending them
+with ``numpy.fft.``. The module defines the following two functions:
+
+1. `numpy.fft.fft <#fft>`__
+2. `numpy.fft.ifft <#ifft>`__
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.fft.ifft.html
+
+fft
+---
+
+Since ``ulab``\ ’s ``ndarray`` does not support complex numbers, the
+invocation of the Fourier transform differs from that in ``numpy``. In
+``numpy``, you can simply pass an array or iterable to the function, and
+it will be treated as a complex array:
+
+.. code::
+
+ # code to be run in CPython
+
+ fft.fft([1, 2, 3, 4, 1, 2, 3, 4])
+
+
+
+.. parsed-literal::
+
+ array([20.+0.j, 0.+0.j, -4.+4.j, 0.+0.j, -4.+0.j, 0.+0.j, -4.-4.j,
+ 0.+0.j])
+
+
+
+**WARNING:** The array returned is also complex, i.e., the real and
+imaginary components are cast together. In ``ulab``, the real and
+imaginary parts are treated separately: you have to pass two
+``ndarray``\ s to the function, although, the second argument is
+optional, in which case the imaginary part is assumed to be zero.
+
+**WARNING:** The function, as opposed to ``numpy``, returns a 2-tuple,
+whose elements are two ``ndarray``\ s, holding the real and imaginary
+parts of the transform separately.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.linspace(0, 10, num=1024)
+ y = np.sin(x)
+ z = np.zeros(len(x))
+
+ a, b = np.fft.fft(x)
+ print('real part:\t', a)
+ print('\nimaginary part:\t', b)
+
+ c, d = np.fft.fft(x, z)
+ print('\nreal part:\t', c)
+ print('\nimaginary part:\t', d)
+
+.. parsed-literal::
+
+ real part: array([5119.996, -5.004663, -5.004798, ..., -5.005482, -5.005643, -5.006577], dtype=float)
+
+ imaginary part: array([0.0, 1631.333, 815.659, ..., -543.764, -815.6588, -1631.333], dtype=float)
+
+ real part: array([5119.996, -5.004663, -5.004798, ..., -5.005482, -5.005643, -5.006577], dtype=float)
+
+ imaginary part: array([0.0, 1631.333, 815.659, ..., -543.764, -815.6588, -1631.333], dtype=float)
+
+
+
+ulab with complex support
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the ``ULAB_SUPPORTS_COMPLEX``, and ``ULAB_FFT_IS_NUMPY_COMPATIBLE``
+pre-processor constants are set to 1 in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__
+as
+
+.. code:: c
+
+ // Adds support for complex ndarrays
+ #ifndef ULAB_SUPPORTS_COMPLEX
+ #define ULAB_SUPPORTS_COMPLEX (1)
+ #endif
+
+.. code:: c
+
+ #ifndef ULAB_FFT_IS_NUMPY_COMPATIBLE
+ #define ULAB_FFT_IS_NUMPY_COMPATIBLE (1)
+ #endif
+
+then the FFT routine will behave in a ``numpy``-compatible way: the
+single input array can either be real, in which case the imaginary part
+is assumed to be zero, or complex. The output is also complex.
+
+While ``numpy``-compatibility might be a desired feature, it has one
+side effect, namely, the FFT routine consumes approx. 50% more RAM. The
+reason for this lies in the implementation details.
+
+ifft
+----
+
+The above-mentioned rules apply to the inverse Fourier transform. The
+inverse is also normalised by ``N``, the number of elements, as is
+customary in ``numpy``. With the normalisation, we can ascertain that
+the inverse of the transform is equal to the original array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.linspace(0, 10, num=1024)
+ y = np.sin(x)
+
+ a, b = np.fft.fft(y)
+
+ print('original vector:\t', y)
+
+ y, z = np.fft.ifft(a, b)
+ # the real part should be equal to y
+ print('\nreal part of inverse:\t', y)
+ # the imaginary part should be equal to zero
+ print('\nimaginary part of inverse:\t', z)
+
+.. parsed-literal::
+
+ original vector: array([0.0, 0.009775016, 0.0195491, ..., -0.5275068, -0.5357859, -0.5440139], dtype=float)
+
+ real part of inverse: array([-2.980232e-08, 0.0097754, 0.0195494, ..., -0.5275064, -0.5357857, -0.5440133], dtype=float)
+
+ imaginary part of inverse: array([-2.980232e-08, -1.451171e-07, 3.693752e-08, ..., 6.44871e-08, 9.34986e-08, 2.18336e-07], dtype=float)
+
+
+
+Note that unlike in ``numpy``, the length of the array on which the
+Fourier transform is carried out must be a power of 2. If this is not
+the case, the function raises a ``ValueError`` exception.
+
+ulab with complex support
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``fft.ifft`` function can also be made ``numpy``-compatible by
+setting the ``ULAB_SUPPORTS_COMPLEX``, and
+``ULAB_FFT_IS_NUMPY_COMPATIBLE`` pre-processor constants to 1.
+
+Computation and storage costs
+-----------------------------
+
+RAM
+~~~
+
+The FFT routine of ``ulab`` calculates the transform in place. This
+means that beyond reserving space for the two ``ndarray``\ s that will
+be returned (the computation uses these two as intermediate storage
+space), only a handful of temporary variables, all floats or 32-bit
+integers, are required.
+
+Speed of FFTs
+~~~~~~~~~~~~~
+
+A comment on the speed: a 1024-point transform implemented in python
+would cost around 90 ms, and 13 ms in assembly, if the code runs on the
+pyboard, v.1.1. You can gain a factor of four by moving to the D series
+https://github.com/peterhinch/micropython-fourier/blob/master/README.md#8-performance.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.linspace(0, 10, num=1024)
+ y = np.sin(x)
+
+ @timeit
+ def np_fft(y):
+ return np.fft.fft(y)
+
+ a, b = np_fft(y)
+
+.. parsed-literal::
+
+ execution time: 1985 us
+
+
+
+The C implementation runs in less than 2 ms on the pyboard (we have just
+measured that), and has been reported to run in under 0.8 ms on the D
+series board. That is an improvement of at least a factor of four.
diff --git a/circuitpython/extmod/ulab/docs/manual/source/numpy-functions.rst b/circuitpython/extmod/ulab/docs/manual/source/numpy-functions.rst
new file mode 100644
index 0000000..206d641
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/numpy-functions.rst
@@ -0,0 +1,1664 @@
+
+Numpy functions
+===============
+
+This section of the manual discusses those functions that were adapted
+from ``numpy``. Starred functions accept complex arrays as arguments, if
+the firmware was compiled with complex support.
+
+1. `numpy.all\* <#all>`__
+2. `numpy.any\* <#any>`__
+3. `numpy.argmax <#argmax>`__
+4. `numpy.argmin <#argmin>`__
+5. `numpy.argsort <#argsort>`__
+6. `numpy.clip <#clip>`__
+7. `numpy.compress\* <#compress>`__
+8. `numpy.conjugate\* <#conjugate>`__
+9. `numpy.convolve\* <#convolve>`__
+10. `numpy.diff <#diff>`__
+11. `numpy.dot <#dot>`__
+12. `numpy.equal <#equal>`__
+13. `numpy.flip\* <#flip>`__
+14. `numpy.imag\* <#imag>`__
+15. `numpy.interp <#interp>`__
+16. `numpy.isfinite <#isfinite>`__
+17. `numpy.isinf <#isinf>`__
+18. `numpy.max <#max>`__
+19. `numpy.maximum <#maximum>`__
+20. `numpy.mean <#mean>`__
+21. `numpy.median <#median>`__
+22. `numpy.min <#min>`__
+23. `numpy.minimum <#minimum>`__
+24. `numpy.not_equal <#equal>`__
+25. `numpy.polyfit <#polyfit>`__
+26. `numpy.polyval <#polyval>`__
+27. `numpy.real\* <#real>`__
+28. `numpy.roll <#roll>`__
+29. `numpy.sort <#sort>`__
+30. `numpy.sort_complex\* <#sort_complex>`__
+31. `numpy.std <#std>`__
+32. `numpy.sum <#sum>`__
+33. `numpy.trace <#trace>`__
+34. `numpy.trapz <#trapz>`__
+35. `numpy.where <#where>`__
+
+all
+---
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.all.html
+
+The function takes one positional, and one keyword argument, the
+``axis``, with a default value of ``None``, and tests, whether *all*
+array elements along the given axis evaluate to ``True``. If the keyword
+argument is ``None``, the flattened array is inspected.
+
+Elements of an array evaluate to ``True``, if they are not equal to
+zero, or the Boolean ``False``. The return value if a Boolean
+``ndarray``.
+
+If the firmware was compiled with complex support, the function can
+accept complex arrays.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(12)).reshape((3, 4))
+
+ print('\na:\n', a)
+
+ b = np.all(a)
+ print('\nall of the flattened array:\n', b)
+
+ c = np.all(a, axis=0)
+ print('\nall of a along 0th axis:\n', c)
+
+ d = np.all(a, axis=1)
+ print('\nall of a along 1st axis:\n', d)
+
+.. parsed-literal::
+
+
+ a:
+ array([[0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0]], dtype=float64)
+
+ all of the flattened array:
+ False
+
+ all of a along 0th axis:
+ array([False, True, True, True], dtype=bool)
+
+ all of a along 1st axis:
+ array([False, True, True], dtype=bool)
+
+
+
+
+any
+---
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.any.html
+
+The function takes one positional, and one keyword argument, the
+``axis``, with a default value of ``None``, and tests, whether *any*
+array element along the given axis evaluates to ``True``. If the keyword
+argument is ``None``, the flattened array is inspected.
+
+Elements of an array evaluate to ``True``, if they are not equal to
+zero, or the Boolean ``False``. The return value if a Boolean
+``ndarray``.
+
+If the firmware was compiled with complex support, the function can
+accept complex arrays.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(12)).reshape((3, 4))
+
+ print('\na:\n', a)
+
+ b = np.any(a)
+ print('\nany of the flattened array:\n', b)
+
+ c = np.any(a, axis=0)
+ print('\nany of a along 0th axis:\n', c)
+
+ d = np.any(a, axis=1)
+ print('\nany of a along 1st axis:\n', d)
+
+.. parsed-literal::
+
+
+ a:
+ array([[0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0]], dtype=float64)
+
+ any of the flattened array:
+ True
+
+ any of a along 0th axis:
+ array([True, True, True, True], dtype=bool)
+
+ any of a along 1st axis:
+ array([True, True, True], dtype=bool)
+
+
+
+
+argmax
+------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html
+
+See `numpy.max <#max>`__.
+
+argmin
+------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmin.html
+
+See `numpy.max <#max>`__.
+
+argsort
+-------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.argsort.html
+
+Similarly to `sort <#sort>`__, ``argsort`` takes a positional, and a
+keyword argument, and returns an unsigned short index array of type
+``ndarray`` with the same dimensions as the input, or, if ``axis=None``,
+as a row vector with length equal to the number of elements in the input
+(i.e., the flattened array). The indices in the output sort the input in
+ascending order. The routine in ``argsort`` is the same as in ``sort``,
+therefore, the comments on computational expenses (time and RAM) also
+apply. In particular, since no copy of the original data is required,
+virtually no RAM beyond the output array is used.
+
+Since the underlying container of the output array is of type
+``uint16_t``, neither of the output dimensions should be larger than
+65535. If that happens to be the case, the function will bail out with a
+``ValueError``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)
+ print('\na:\n', a)
+ b = np.argsort(a, axis=0)
+ print('\na sorted along vertical axis:\n', b)
+
+ c = np.argsort(a, axis=1)
+ print('\na sorted along horizontal axis:\n', c)
+
+ c = np.argsort(a, axis=None)
+ print('\nflattened a sorted:\n', c)
+
+.. parsed-literal::
+
+
+ a:
+ array([[1.0, 12.0, 3.0, 0.0],
+ [5.0, 3.0, 4.0, 1.0],
+ [9.0, 11.0, 1.0, 8.0],
+ [7.0, 10.0, 0.0, 1.0]], dtype=float64)
+
+ a sorted along vertical axis:
+ array([[0, 1, 3, 0],
+ [1, 3, 2, 1],
+ [3, 2, 0, 3],
+ [2, 0, 1, 2]], dtype=uint16)
+
+ a sorted along horizontal axis:
+ array([[3, 0, 2, 1],
+ [3, 1, 2, 0],
+ [2, 3, 0, 1],
+ [2, 3, 0, 1]], dtype=uint16)
+
+ Traceback (most recent call last):
+ File "/dev/shm/micropython.py", line 12, in <module>
+ NotImplementedError: argsort is not implemented for flattened arrays
+
+
+
+Since during the sorting, only the indices are shuffled, ``argsort``
+does not modify the input array, as one can verify this by the following
+example:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([0, 5, 1, 3, 2, 4], dtype=np.uint8)
+ print('\na:\n', a)
+ b = np.argsort(a, axis=0)
+ print('\nsorting indices:\n', b)
+ print('\nthe original array:\n', a)
+
+.. parsed-literal::
+
+
+ a:
+ array([0, 5, 1, 3, 2, 4], dtype=uint8)
+
+ sorting indices:
+ array([0, 2, 4, 3, 5, 1], dtype=uint16)
+
+ the original array:
+ array([0, 5, 1, 3, 2, 4], dtype=uint8)
+
+
+
+
+clip
+----
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html
+
+Clips an array, i.e., values that are outside of an interval are clipped
+to the interval edges. The function is equivalent to
+``maximum(a_min, minimum(a, a_max))`` broadcasting takes place exactly
+as in `minimum <#minimum>`__. If the arrays are of different ``dtype``,
+the output is upcast as in `Binary operators <#Binary-operators>`__.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.uint8)
+ print('a:\t\t', a)
+ print('clipped:\t', np.clip(a, 3, 7))
+
+ b = 3 * np.ones(len(a), dtype=np.float)
+ print('\na:\t\t', a)
+ print('b:\t\t', b)
+ print('clipped:\t', np.clip(a, b, 7))
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)
+ clipped: array([3, 3, 3, 3, 4, 5, 6, 7, 7], dtype=uint8)
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)
+ b: array([3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=float64)
+ clipped: array([3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 7.0], dtype=float64)
+
+
+
+
+compress
+--------
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.compress.html
+
+The function returns selected slices of an array along given axis. If
+the axis keyword is ``None``, the flattened array is used.
+
+If the firmware was compiled with complex support, the function can
+accept complex arguments.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(6)).reshape((2, 3))
+
+ print('a:\n', a)
+ print('\ncompress(a):\n', np.compress([0, 1], a, axis=0))
+
+.. parsed-literal::
+
+ a:
+ array([[0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0]], dtype=float64)
+
+ compress(a):
+ array([[3.0, 4.0, 5.0]], dtype=float64)
+
+
+
+
+conjugate
+---------
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.conjugate.html
+
+If the firmware was compiled with complex support, the function
+calculates the complex conjugate of the input array. If the input array
+is of real ``dtype``, then the output is simply a copy, preserving the
+``dtype``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.uint8)
+ b = np.array([1+1j, 2-2j, 3+3j, 4-4j], dtype=np.complex)
+
+ print('a:\t\t', a)
+ print('conjugate(a):\t', np.conjugate(a))
+ print()
+ print('b:\t\t', b)
+ print('conjugate(b):\t', np.conjugate(b))
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4], dtype=uint8)
+ conjugate(a): array([1, 2, 3, 4], dtype=uint8)
+
+ b: array([1.0+1.0j, 2.0-2.0j, 3.0+3.0j, 4.0-4.0j], dtype=complex)
+ conjugate(b): array([1.0-1.0j, 2.0+2.0j, 3.0-3.0j, 4.0+4.0j], dtype=complex)
+
+
+
+
+convolve
+--------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.convolve.html
+
+Returns the discrete, linear convolution of two one-dimensional arrays.
+
+Only the ``full`` mode is supported, and the ``mode`` named parameter is
+not accepted. Note that all other modes can be had by slicing a ``full``
+result.
+
+If the firmware was compiled with complex support, the function can
+accept complex arrays.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.array((1, 2, 3))
+ y = np.array((1, 10, 100, 1000))
+
+ print(np.convolve(x, y))
+
+.. parsed-literal::
+
+ array([1.0, 12.0, 123.0, 1230.0, 2300.0, 3000.0], dtype=float64)
+
+
+
+
+diff
+----
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.diff.html
+
+The ``diff`` function returns the numerical derivative of the forward
+scheme, or more accurately, the differences of an ``ndarray`` along a
+given axis. The order of derivative can be stipulated with the ``n``
+keyword argument, which should be between 0, and 9. Default is 1. If
+higher order derivatives are required, they can be gotten by repeated
+calls to the function. The ``axis`` keyword argument should be -1 (last
+axis, in ``ulab`` equivalent to the second axis, and this also happens
+to be the default value), 0, or 1.
+
+Beyond the output array, the function requires only a couple of bytes of
+extra RAM for the differentiation stencil. (The stencil is an ``int8``
+array, one byte longer than ``n``. This also explains, why the highest
+order is 9: the coefficients of a ninth-order stencil all fit in signed
+bytes, while 10 would require ``int16``.) Note that as usual in
+numerical differentiation (and also in ``numpy``), the length of the
+respective axis will be reduced by ``n`` after the operation. If ``n``
+is larger than, or equal to the length of the axis, an empty array will
+be returned.
+
+**WARNING**: the ``diff`` function does not implement the ``prepend``
+and ``append`` keywords that can be found in ``numpy``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.uint8)
+ a[3] = 10
+ print('a:\n', a)
+
+ print('\nfirst derivative:\n', np.diff(a, n=1))
+ print('\nsecond derivative:\n', np.diff(a, n=2))
+
+ c = np.array([[1, 2, 3, 4], [4, 3, 2, 1], [1, 4, 9, 16], [0, 0, 0, 0]])
+ print('\nc:\n', c)
+ print('\nfirst derivative, first axis:\n', np.diff(c, axis=0))
+ print('\nfirst derivative, second axis:\n', np.diff(c, axis=1))
+
+.. parsed-literal::
+
+ a:
+ array([0, 1, 2, 10, 4, 5, 6, 7, 8], dtype=uint8)
+
+ first derivative:
+ array([1, 1, 8, 250, 1, 1, 1, 1], dtype=uint8)
+
+ second derivative:
+ array([0, 249, 14, 249, 0, 0, 0], dtype=uint8)
+
+ c:
+ array([[1.0, 2.0, 3.0, 4.0],
+ [4.0, 3.0, 2.0, 1.0],
+ [1.0, 4.0, 9.0, 16.0],
+ [0.0, 0.0, 0.0, 0.0]], dtype=float64)
+
+ first derivative, first axis:
+ array([[3.0, 1.0, -1.0, -3.0],
+ [-3.0, 1.0, 7.0, 15.0],
+ [-1.0, -4.0, -9.0, -16.0]], dtype=float64)
+
+ first derivative, second axis:
+ array([[1.0, 1.0, 1.0],
+ [-1.0, -1.0, -1.0],
+ [3.0, 5.0, 7.0],
+ [0.0, 0.0, 0.0]], dtype=float64)
+
+
+
+
+dot
+---
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html
+
+**WARNING:** numpy applies upcasting rules for the multiplication of
+matrices, while ``ulab`` simply returns a float matrix.
+
+Once you can invert a matrix, you might want to know, whether the
+inversion is correct. You can simply take the original matrix and its
+inverse, and multiply them by calling the ``dot`` function, which takes
+the two matrices as its arguments. If the matrix dimensions do not
+match, the function raises a ``ValueError``. The result of the
+multiplication is expected to be the unit matrix, which is demonstrated
+below.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ m = np.array([[1, 2, 3], [4, 5, 6], [7, 10, 9]], dtype=np.uint8)
+ n = np.linalg.inv(m)
+ print("m:\n", m)
+ print("\nm^-1:\n", n)
+ # this should be the unit matrix
+ print("\nm*m^-1:\n", np.dot(m, n))
+
+.. parsed-literal::
+
+ m:
+ array([[1, 2, 3],
+ [4, 5, 6],
+ [7, 10, 9]], dtype=uint8)
+
+ m^-1:
+ array([[-1.25, 1.0, -0.25],
+ [0.4999999999999998, -1.0, 0.5],
+ [0.4166666666666668, 0.3333333333333333, -0.25]], dtype=float64)
+
+ m*m^-1:
+ array([[1.0, 0.0, 0.0],
+ [4.440892098500626e-16, 1.0, 0.0],
+ [8.881784197001252e-16, 0.0, 1.0]], dtype=float64)
+
+
+
+
+Note that for matrix multiplication you don’t necessarily need square
+matrices, it is enough, if their dimensions are compatible (i.e., the
+the left-hand-side matrix has as many columns, as does the
+right-hand-side matrix rows):
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ m = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.uint8)
+ n = np.array([[1, 2], [3, 4], [5, 6], [7, 8]], dtype=np.uint8)
+ print(m)
+ print(n)
+ print(np.dot(m, n))
+
+.. parsed-literal::
+
+ array([[1, 2, 3, 4],
+ [5, 6, 7, 8]], dtype=uint8)
+ array([[1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8]], dtype=uint8)
+ array([[50.0, 60.0],
+ [114.0, 140.0]], dtype=float64)
+
+
+
+
+equal
+-----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.equal.html
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.not_equal.html
+
+In ``micropython``, equality of arrays or scalars can be established by
+utilising the ``==``, ``!=``, ``<``, ``>``, ``<=``, or ``=>`` binary
+operators. In ``circuitpython``, ``==`` and ``!=`` will produce
+unexpected results. In order to avoid this discrepancy, and to maintain
+compatibility with ``numpy``, ``ulab`` implements the ``equal`` and
+``not_equal`` operators that return the same results, irrespective of
+the ``python`` implementation.
+
+These two functions take two ``ndarray``\ s, or scalars as their
+arguments. No keyword arguments are implemented.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9))
+ b = np.zeros(9)
+
+ print('a: ', a)
+ print('b: ', b)
+ print('\na == b: ', np.equal(a, b))
+ print('a != b: ', np.not_equal(a, b))
+
+ # comparison with scalars
+ print('a == 2: ', np.equal(a, 2))
+
+.. parsed-literal::
+
+ a: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+ b: array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=float64)
+
+ a == b: array([True, False, False, False, False, False, False, False, False], dtype=bool)
+ a != b: array([False, True, True, True, True, True, True, True, True], dtype=bool)
+ a == 2: array([False, False, True, False, False, False, False, False, False], dtype=bool)
+
+
+
+
+flip
+----
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html
+
+The ``flip`` function takes one positional, an ``ndarray``, and one
+keyword argument, ``axis = None``, and reverses the order of elements
+along the given axis. If the keyword argument is ``None``, the matrix’
+entries are flipped along all axes. ``flip`` returns a new copy of the
+array.
+
+If the firmware was compiled with complex support, the function can
+accept complex arrays.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5])
+ print("a: \t", a)
+ print("a flipped:\t", np.flip(a))
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)
+ print("\na flipped horizontally\n", np.flip(a, axis=1))
+ print("\na flipped vertically\n", np.flip(a, axis=0))
+ print("\na flipped horizontally+vertically\n", np.flip(a))
+
+.. parsed-literal::
+
+ a: array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=float64)
+ a flipped: array([5.0, 4.0, 3.0, 2.0, 1.0], dtype=float64)
+
+ a flipped horizontally
+ array([[3, 2, 1],
+ [6, 5, 4],
+ [9, 8, 7]], dtype=uint8)
+
+ a flipped vertically
+ array([[7, 8, 9],
+ [4, 5, 6],
+ [1, 2, 3]], dtype=uint8)
+
+ a flipped horizontally+vertically
+ array([9, 8, 7, 6, 5, 4, 3, 2, 1], dtype=uint8)
+
+
+
+
+imag
+----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.imag.html
+
+The ``imag`` function returns the imaginary part of an array, or scalar.
+It cannot accept a generic iterable as its argument. The function is
+defined only, if the firmware was compiled with complex support.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3], dtype=np.uint16)
+ print("a:\t\t", a)
+ print("imag(a):\t", np.imag(a))
+
+ b = np.array([1, 2+1j, 3-1j], dtype=np.complex)
+ print("\nb:\t\t", b)
+ print("imag(b):\t", np.imag(b))
+
+.. parsed-literal::
+
+ a: array([1, 2, 3], dtype=uint16)
+ imag(a): array([0, 0, 0], dtype=uint16)
+
+ b: array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)
+ imag(b): array([0.0, 1.0, -1.0], dtype=float64)
+
+
+
+
+interp
+------
+
+``numpy``: https://docs.scipy.org/doc/numpy/numpy.interp
+
+The ``interp`` function returns the linearly interpolated values of a
+one-dimensional numerical array. It requires three positional
+arguments,\ ``x``, at which the interpolated values are evaluated,
+``xp``, the array of the independent data variable, and ``fp``, the
+array of the dependent values of the data. ``xp`` must be a
+monotonically increasing sequence of numbers.
+
+Two keyword arguments, ``left``, and ``right`` can also be supplied;
+these determine the return values, if ``x < xp[0]``, and ``x > xp[-1]``,
+respectively. If these arguments are not supplied, ``left``, and
+``right`` default to ``fp[0]``, and ``fp[-1]``, respectively.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.array([1, 2, 3, 4, 5]) - 0.2
+ xp = np.array([1, 2, 3, 4])
+ fp = np.array([1, 2, 3, 5])
+
+ print(x)
+ print(np.interp(x, xp, fp))
+ print(np.interp(x, xp, fp, left=0.0))
+ print(np.interp(x, xp, fp, right=10.0))
+
+.. parsed-literal::
+
+ array([0.8, 1.8, 2.8, 3.8, 4.8], dtype=float64)
+ array([1.0, 1.8, 2.8, 4.6, 5.0], dtype=float64)
+ array([0.0, 1.8, 2.8, 4.6, 5.0], dtype=float64)
+ array([1.0, 1.8, 2.8, 4.6, 10.0], dtype=float64)
+
+
+
+
+isfinite
+--------
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.isfinite.html
+
+Returns a Boolean array of the same shape as the input, or a
+``True/False``, if the input is a scalar. In the return value, all
+elements are ``True`` at positions, where the input value was finite.
+Integer types are automatically finite, therefore, if the input is of
+integer type, the output will be the ``True`` tensor.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print('isfinite(0): ', np.isfinite(0))
+
+ a = np.array([1, 2, np.nan])
+ print('\n' + '='*20)
+ print('a:\n', a)
+ print('\nisfinite(a):\n', np.isfinite(a))
+
+ b = np.array([1, 2, np.inf])
+ print('\n' + '='*20)
+ print('b:\n', b)
+ print('\nisfinite(b):\n', np.isfinite(b))
+
+ c = np.array([1, 2, 3], dtype=np.uint16)
+ print('\n' + '='*20)
+ print('c:\n', c)
+ print('\nisfinite(c):\n', np.isfinite(c))
+
+.. parsed-literal::
+
+ isfinite(0): True
+
+ ====================
+ a:
+ array([1.0, 2.0, nan], dtype=float64)
+
+ isfinite(a):
+ array([True, True, False], dtype=bool)
+
+ ====================
+ b:
+ array([1.0, 2.0, inf], dtype=float64)
+
+ isfinite(b):
+ array([True, True, False], dtype=bool)
+
+ ====================
+ c:
+ array([1, 2, 3], dtype=uint16)
+
+ isfinite(c):
+ array([True, True, True], dtype=bool)
+
+
+
+
+isinf
+-----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.isinf.html
+
+Similar to `isfinite <#isfinite>`__, but the output is ``True`` at
+positions, where the input is infinite. Integer types return the
+``False`` tensor.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print('isinf(0): ', np.isinf(0))
+
+ a = np.array([1, 2, np.nan])
+ print('\n' + '='*20)
+ print('a:\n', a)
+ print('\nisinf(a):\n', np.isinf(a))
+
+ b = np.array([1, 2, np.inf])
+ print('\n' + '='*20)
+ print('b:\n', b)
+ print('\nisinf(b):\n', np.isinf(b))
+
+ c = np.array([1, 2, 3], dtype=np.uint16)
+ print('\n' + '='*20)
+ print('c:\n', c)
+ print('\nisinf(c):\n', np.isinf(c))
+
+.. parsed-literal::
+
+ isinf(0): False
+
+ ====================
+ a:
+ array([1.0, 2.0, nan], dtype=float64)
+
+ isinf(a):
+ array([False, False, False], dtype=bool)
+
+ ====================
+ b:
+ array([1.0, 2.0, inf], dtype=float64)
+
+ isinf(b):
+ array([False, False, True], dtype=bool)
+
+ ====================
+ c:
+ array([1, 2, 3], dtype=uint16)
+
+ isinf(c):
+ array([False, False, False], dtype=bool)
+
+
+
+
+mean
+----
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.mean.html
+
+If the axis keyword is not specified, it assumes the default value of
+``None``, and returns the result of the computation for the flattened
+array. Otherwise, the calculation is along the given axis.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+ print('a: \n', a)
+ print('mean, flat: ', np.mean(a))
+ print('mean, horizontal: ', np.mean(a, axis=1))
+ print('mean, vertical: ', np.mean(a, axis=0))
+
+.. parsed-literal::
+
+ a:
+ array([[1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0],
+ [7.0, 8.0, 9.0]], dtype=float64)
+ mean, flat: 5.0
+ mean, horizontal: array([2.0, 5.0, 8.0], dtype=float64)
+ mean, vertical: array([4.0, 5.0, 6.0], dtype=float64)
+
+
+
+
+max
+---
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.max.html
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.min.html
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmin.html
+
+**WARNING:** Difference to ``numpy``: the ``out`` keyword argument is
+not implemented.
+
+These functions follow the same pattern, and work with generic
+iterables, and ``ndarray``\ s. ``min``, and ``max`` return the minimum
+or maximum of a sequence. If the input array is two-dimensional, the
+``axis`` keyword argument can be supplied, in which case the
+minimum/maximum along the given axis will be returned. If ``axis=None``
+(this is also the default value), the minimum/maximum of the flattened
+array will be determined.
+
+``argmin/argmax`` return the position (index) of the minimum/maximum in
+the sequence.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 0, 1, 10])
+ print('a:', a)
+ print('min of a:', np.min(a))
+ print('argmin of a:', np.argmin(a))
+
+ b = np.array([[1, 2, 0], [1, 10, -1]])
+ print('\nb:\n', b)
+ print('min of b (flattened):', np.min(b))
+ print('min of b (axis=0):', np.min(b, axis=0))
+ print('min of b (axis=1):', np.min(b, axis=1))
+
+.. parsed-literal::
+
+ a: array([1.0, 2.0, 0.0, 1.0, 10.0], dtype=float64)
+ min of a: 0.0
+ argmin of a: 2
+
+ b:
+ array([[1.0, 2.0, 0.0],
+ [1.0, 10.0, -1.0]], dtype=float64)
+ min of b (flattened): -1.0
+ min of b (axis=0): array([1.0, 2.0, -1.0], dtype=float64)
+ min of b (axis=1): array([0.0, -1.0], dtype=float64)
+
+
+
+
+median
+------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.median.html
+
+The function computes the median along the specified axis, and returns
+the median of the array elements. If the ``axis`` keyword argument is
+``None``, the arrays is flattened first. The ``dtype`` of the results is
+always float.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(12), dtype=np.int8).reshape((3, 4))
+ print('a:\n', a)
+ print('\nmedian of the flattened array: ', np.median(a))
+ print('\nmedian along the vertical axis: ', np.median(a, axis=0))
+ print('\nmedian along the horizontal axis: ', np.median(a, axis=1))
+
+.. parsed-literal::
+
+ a:
+ array([[0, 1, 2, 3],
+ [4, 5, 6, 7],
+ [8, 9, 10, 11]], dtype=int8)
+
+ median of the flattened array: 5.5
+
+ median along the vertical axis: array([4.0, 5.0, 6.0, 7.0], dtype=float64)
+
+ median along the horizontal axis: array([1.5, 5.5, 9.5], dtype=float64)
+
+
+
+
+min
+---
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.min.html
+
+See `numpy.max <#max>`__.
+
+minimum
+-------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.minimum.html
+
+See `numpy.maximum <#maximum>`__
+
+maximum
+-------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.maximum.html
+
+Returns the maximum of two arrays, or two scalars, or an array, and a
+scalar. If the arrays are of different ``dtype``, the output is upcast
+as in `Binary operators <#Binary-operators>`__. If both inputs are
+scalars, a scalar is returned. Only positional arguments are
+implemented.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)
+ b = np.array([5, 4, 3, 2, 1], dtype=np.float)
+ print('minimum of a, and b:')
+ print(np.minimum(a, b))
+
+ print('\nmaximum of a, and b:')
+ print(np.maximum(a, b))
+
+ print('\nmaximum of 1, and 5.5:')
+ print(np.maximum(1, 5.5))
+
+.. parsed-literal::
+
+ minimum of a, and b:
+ array([1.0, 2.0, 3.0, 2.0, 1.0], dtype=float64)
+
+ maximum of a, and b:
+ array([5.0, 4.0, 3.0, 4.0, 5.0], dtype=float64)
+
+ maximum of 1, and 5.5:
+ 5.5
+
+
+
+
+not_equal
+---------
+
+See `numpy.equal <#equal>`__.
+
+polyfit
+-------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html
+
+polyfit takes two, or three arguments. The last one is the degree of the
+polynomial that will be fitted, the last but one is an array or iterable
+with the ``y`` (dependent) values, and the first one, an array or
+iterable with the ``x`` (independent) values, can be dropped. If that is
+the case, ``x`` will be generated in the function as ``range(len(y))``.
+
+If the lengths of ``x``, and ``y`` are not the same, the function raises
+a ``ValueError``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.array([0, 1, 2, 3, 4, 5, 6])
+ y = np.array([9, 4, 1, 0, 1, 4, 9])
+ print('independent values:\t', x)
+ print('dependent values:\t', y)
+ print('fitted values:\t\t', np.polyfit(x, y, 2))
+
+ # the same with missing x
+ print('\ndependent values:\t', y)
+ print('fitted values:\t\t', np.polyfit(y, 2))
+
+.. parsed-literal::
+
+ independent values: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=float64)
+ dependent values: array([9.0, 4.0, 1.0, 0.0, 1.0, 4.0, 9.0], dtype=float64)
+ fitted values: array([1.0, -6.0, 9.000000000000004], dtype=float64)
+
+ dependent values: array([9.0, 4.0, 1.0, 0.0, 1.0, 4.0, 9.0], dtype=float64)
+ fitted values: array([1.0, -6.0, 9.000000000000004], dtype=float64)
+
+
+
+
+Execution time
+~~~~~~~~~~~~~~
+
+``polyfit`` is based on the inversion of a matrix (there is more on the
+background in https://en.wikipedia.org/wiki/Polynomial_regression), and
+it requires the intermediate storage of ``2*N*(deg+1)`` floats, where
+``N`` is the number of entries in the input array, and ``deg`` is the
+fit’s degree. The additional computation costs of the matrix inversion
+discussed in `linalg.inv <#inv>`__ also apply. The example from above
+needs around 150 microseconds to return:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ @timeit
+ def time_polyfit(x, y, n):
+ return np.polyfit(x, y, n)
+
+ x = np.array([0, 1, 2, 3, 4, 5, 6])
+ y = np.array([9, 4, 1, 0, 1, 4, 9])
+
+ time_polyfit(x, y, 2)
+
+.. parsed-literal::
+
+ execution time: 153 us
+
+
+polyval
+-------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyval.html
+
+``polyval`` takes two arguments, both arrays or generic ``micropython``
+iterables returning scalars.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ p = [1, 1, 1, 0]
+ x = [0, 1, 2, 3, 4]
+ print('coefficients: ', p)
+ print('independent values: ', x)
+ print('\nvalues of p(x): ', np.polyval(p, x))
+
+ # the same works with one-dimensional ndarrays
+ a = np.array(x)
+ print('\nndarray (a): ', a)
+ print('value of p(a): ', np.polyval(p, a))
+
+.. parsed-literal::
+
+ coefficients: [1, 1, 1, 0]
+ independent values: [0, 1, 2, 3, 4]
+
+ values of p(x): array([0.0, 3.0, 14.0, 39.0, 84.0], dtype=float64)
+
+ ndarray (a): array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float64)
+ value of p(a): array([0.0, 3.0, 14.0, 39.0, 84.0], dtype=float64)
+
+
+
+
+real
+----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.real.html
+
+The ``real`` function returns the real part of an array, or scalar. It
+cannot accept a generic iterable as its argument. The function is
+defined only, if the firmware was compiled with complex support.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3], dtype=np.uint16)
+ print("a:\t\t", a)
+ print("real(a):\t", np.real(a))
+
+ b = np.array([1, 2+1j, 3-1j], dtype=np.complex)
+ print("\nb:\t\t", b)
+ print("real(b):\t", np.real(b))
+
+.. parsed-literal::
+
+ a: array([1, 2, 3], dtype=uint16)
+ real(a): array([1, 2, 3], dtype=uint16)
+
+ b: array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)
+ real(b): array([1.0, 2.0, 3.0], dtype=float64)
+
+
+
+
+roll
+----
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.roll.html
+
+The roll function shifts the content of a vector by the positions given
+as the second argument. If the ``axis`` keyword is supplied, the shift
+is applied to the given axis.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5, 6, 7, 8])
+ print("a:\t\t\t", a)
+
+ a = np.roll(a, 2)
+ print("a rolled to the left:\t", a)
+
+ # this should be the original vector
+ a = np.roll(a, -2)
+ print("a rolled to the right:\t", a)
+
+.. parsed-literal::
+
+ a: array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+ a rolled to the left: array([7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=float64)
+ a rolled to the right: array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+
+
+
+
+Rolling works with matrices, too. If the ``axis`` keyword is 0, the
+matrix is rolled along its vertical axis, otherwise, horizontally.
+
+Horizontal rolls are faster, because they require fewer steps, and
+larger memory chunks are copied, however, they also require more RAM:
+basically the whole row must be stored internally. Most expensive are
+the ``None`` keyword values, because with ``axis = None``, the array is
+flattened first, hence the row’s length is the size of the whole matrix.
+
+Vertical rolls require two internal copies of single columns.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(12)).reshape((3, 4))
+ print("a:\n", a)
+ a = np.roll(a, 2, axis=0)
+ print("\na rolled up:\n", a)
+
+ a = np.array(range(12)).reshape((3, 4))
+ print("a:\n", a)
+ a = np.roll(a, -1, axis=1)
+ print("\na rolled to the left:\n", a)
+
+ a = np.array(range(12)).reshape((3, 4))
+ print("a:\n", a)
+ a = np.roll(a, 1, axis=None)
+ print("\na rolled with None:\n", a)
+
+.. parsed-literal::
+
+ a:
+ array([[0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0]], dtype=float64)
+
+ a rolled up:
+ array([[4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0],
+ [0.0, 1.0, 2.0, 3.0]], dtype=float64)
+ a:
+ array([[0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0]], dtype=float64)
+
+ a rolled to the left:
+ array([[1.0, 2.0, 3.0, 0.0],
+ [5.0, 6.0, 7.0, 4.0],
+ [9.0, 10.0, 11.0, 8.0]], dtype=float64)
+ a:
+ array([[0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0]], dtype=float64)
+
+ a rolled with None:
+ array([[11.0, 0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0, 6.0],
+ [7.0, 8.0, 9.0, 10.0]], dtype=float64)
+
+
+
+
+sort
+----
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html
+
+The sort function takes an ndarray, and sorts its elements in ascending
+order along the specified axis using a heap sort algorithm. As opposed
+to the ``.sort()`` method discussed earlier, this function creates a
+copy of its input before sorting, and at the end, returns this copy.
+Sorting takes place in place, without auxiliary storage. The ``axis``
+keyword argument takes on the possible values of -1 (the last axis, in
+``ulab`` equivalent to the second axis, and this also happens to be the
+default value), 0, 1, or ``None``. The first three cases are identical
+to those in `diff <#diff>`__, while the last one flattens the array
+before sorting.
+
+If descending order is required, the result can simply be ``flip``\ ped,
+see `flip <#flip>`__.
+
+**WARNING:** ``numpy`` defines the ``kind``, and ``order`` keyword
+arguments that are not implemented here. The function in ``ulab`` always
+uses heap sort, and since ``ulab`` does not have the concept of data
+fields, the ``order`` keyword argument would have no meaning.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)
+ print('\na:\n', a)
+ b = np.sort(a, axis=0)
+ print('\na sorted along vertical axis:\n', b)
+
+ c = np.sort(a, axis=1)
+ print('\na sorted along horizontal axis:\n', c)
+
+ c = np.sort(a, axis=None)
+ print('\nflattened a sorted:\n', c)
+
+.. parsed-literal::
+
+
+ a:
+ array([[1.0, 12.0, 3.0, 0.0],
+ [5.0, 3.0, 4.0, 1.0],
+ [9.0, 11.0, 1.0, 8.0],
+ [7.0, 10.0, 0.0, 1.0]], dtype=float64)
+
+ a sorted along vertical axis:
+ array([[1.0, 3.0, 0.0, 0.0],
+ [5.0, 10.0, 1.0, 1.0],
+ [7.0, 11.0, 3.0, 1.0],
+ [9.0, 12.0, 4.0, 8.0]], dtype=float64)
+
+ a sorted along horizontal axis:
+ array([[0.0, 1.0, 3.0, 12.0],
+ [1.0, 3.0, 4.0, 5.0],
+ [1.0, 8.0, 9.0, 11.0],
+ [0.0, 1.0, 7.0, 10.0]], dtype=float64)
+
+ flattened a sorted:
+ array([0.0, 0.0, 1.0, ..., 10.0, 11.0, 12.0], dtype=float64)
+
+
+
+
+Heap sort requires :math:`\sim N\log N` operations, and notably, the
+worst case costs only 20% more time than the average. In order to get an
+order-of-magnitude estimate, we will take the sine of 1000 uniformly
+spaced numbers between 0, and two pi, and sort them:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ @timeit
+ def sort_time(array):
+ return nup.sort(array)
+
+ b = np.sin(np.linspace(0, 6.28, num=1000))
+ print('b: ', b)
+ sort_time(b)
+ print('\nb sorted:\n', b)
+sort_complex
+------------
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.sort_complex.html
+
+If the firmware was compiled with complex support, the functions sorts
+the input array first according to its real part, and then the imaginary
+part. The input must be a one-dimensional array. The output is always of
+``dtype`` complex, even if the input was real integer.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([5, 4, 3, 2, 1], dtype=np.int16)
+ print('a:\t\t\t', a)
+ print('sort_complex(a):\t', np.sort_complex(a))
+ print()
+
+ b = np.array([5, 4+3j, 4-2j, 0, 1j], dtype=np.complex)
+ print('b:\t\t\t', b)
+ print('sort_complex(b):\t', np.sort_complex(b))
+
+.. parsed-literal::
+
+ a: array([5, 4, 3, 2, 1], dtype=int16)
+ sort_complex(a): array([1.0+0.0j, 2.0+0.0j, 3.0+0.0j, 4.0+0.0j, 5.0+0.0j], dtype=complex)
+
+ b: array([5.0+0.0j, 4.0+3.0j, 4.0-2.0j, 0.0+0.0j, 0.0+1.0j], dtype=complex)
+ sort_complex(b): array([0.0+0.0j, 0.0+1.0j, 4.0-2.0j, 4.0+3.0j, 5.0+0.0j], dtype=complex)
+
+
+
+
+std
+---
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.std.html
+
+If the axis keyword is not specified, it assumes the default value of
+``None``, and returns the result of the computation for the flattened
+array. Otherwise, the calculation is along the given axis.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+ print('a: \n', a)
+ print('sum, flat array: ', np.std(a))
+ print('std, vertical: ', np.std(a, axis=0))
+ print('std, horizonal: ', np.std(a, axis=1))
+
+.. parsed-literal::
+
+ a:
+ array([[1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0],
+ [7.0, 8.0, 9.0]], dtype=float64)
+ sum, flat array: 2.581988897471611
+ std, vertical: array([2.449489742783178, 2.449489742783178, 2.449489742783178], dtype=float64)
+ std, horizonal: array([0.8164965809277261, 0.8164965809277261, 0.8164965809277261], dtype=float64)
+
+
+
+
+sum
+---
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.sum.html
+
+If the axis keyword is not specified, it assumes the default value of
+``None``, and returns the result of the computation for the flattened
+array. Otherwise, the calculation is along the given axis.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+ print('a: \n', a)
+
+ print('sum, flat array: ', np.sum(a))
+ print('sum, horizontal: ', np.sum(a, axis=1))
+ print('std, vertical: ', np.sum(a, axis=0))
+
+.. parsed-literal::
+
+ a:
+ array([[1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0],
+ [7.0, 8.0, 9.0]], dtype=float64)
+ sum, flat array: 45.0
+ sum, horizontal: array([6.0, 15.0, 24.0], dtype=float64)
+ std, vertical: array([12.0, 15.0, 18.0], dtype=float64)
+
+
+
+
+trace
+-----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.trace.html
+
+The ``trace`` function returns the sum of the diagonal elements of a
+square matrix. If the input argument is not a square matrix, an
+exception will be raised.
+
+The scalar so returned will inherit the type of the input array, i.e.,
+integer arrays have integer trace, and floating point arrays a floating
+point trace.
+
+.. 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]], dtype=np.int8)
+ print('a: ', a)
+ print('\ntrace of a: ', np.trace(a))
+
+ b = np.array([[25, 15, -5], [15, 18, 0], [-5, 0, 11]], dtype=np.float)
+
+ print('='*20 + '\nb: ', b)
+ print('\ntrace of b: ', np.trace(b))
+
+.. parsed-literal::
+
+ a: array([[25, 15, -5],
+ [15, 18, 0],
+ [-5, 0, 11]], dtype=int8)
+
+ trace of a: 54
+ ====================
+ b: array([[25.0, 15.0, -5.0],
+ [15.0, 18.0, 0.0],
+ [-5.0, 0.0, 11.0]], dtype=float64)
+
+ trace of b: 54.0
+
+
+
+
+trapz
+-----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.trapz.html
+
+The function takes one or two one-dimensional ``ndarray``\ s, and
+integrates the dependent values (``y``) using the trapezoidal rule. If
+the independent variable (``x``) is given, that is taken as the sample
+points corresponding to ``y``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ x = np.linspace(0, 9, num=10)
+ y = x*x
+
+ print('x: ', x)
+ print('y: ', y)
+ print('============================')
+ print('integral of y: ', np.trapz(y))
+ print('integral of y at x: ', np.trapz(y, x=x))
+
+.. parsed-literal::
+
+ x: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], dtype=float64)
+ y: array([0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0], dtype=float64)
+ ============================
+ integral of y: 244.5
+ integral of y at x: 244.5
+
+
+
+
+where
+-----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.where.html
+
+The function takes three positional arguments, ``condition``, ``x``, and
+``y``, and returns a new ``ndarray``, whose values are taken from either
+``x``, or ``y``, depending on the truthness of ``condition``. The three
+arguments are broadcast together, and the function raises a
+``ValueError`` exception, if broadcasting is not possible.
+
+The function is implemented for ``ndarray``\ s only: other iterable
+types can be passed after casting them to an ``ndarray`` by calling the
+``array`` constructor.
+
+If the ``dtype``\ s of ``x``, and ``y`` differ, the output is upcast as
+discussed earlier.
+
+Note that the ``condition`` is expanded into an Boolean ``ndarray``.
+This means that the storage required to hold the condition should be
+taken into account, whenever the function is called.
+
+The following example returns an ``ndarray`` of length 4, with 1 at
+positions, where ``condition`` is smaller than 3, and with -1 otherwise.
+
+.. code::
+
+ # code to be run in micropython
+
+
+ from ulab import numpy as np
+
+ condition = np.array([1, 2, 3, 4], dtype=np.uint8)
+ print(np.where(condition < 3, 1, -1))
+
+.. parsed-literal::
+
+ array([1, 1, -1, -1], dtype=int16)
+
+
+
+
+The next snippet shows, how values from two arrays can be fed into the
+output:
+
+.. code::
+
+ # code to be run in micropython
+
+
+ from ulab import numpy as np
+
+ condition = np.array([1, 2, 3, 4], dtype=np.uint8)
+ x = np.array([11, 22, 33, 44], dtype=np.uint8)
+ y = np.array([1, 2, 3, 4], dtype=np.uint8)
+ print(np.where(condition < 3, x, y))
+
+.. parsed-literal::
+
+ array([11, 22, 3, 4], dtype=uint8)
+
+
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/numpy-linalg.rst b/circuitpython/extmod/ulab/docs/manual/source/numpy-linalg.rst
new file mode 100644
index 0000000..8439f33
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/numpy-linalg.rst
@@ -0,0 +1,386 @@
+
+numpy.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.eig <#eig>`__
+4. `numpy.linalg.inv <#inv>`__
+5. `numpy.linalg.norm <#norm>`__
+6. `numpy.linalg.qr <#qr>`__
+
+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
+
+
+
+
+qr
+--
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.linalg.qr.html
+
+The function computes the QR decomposition of a matrix ``m`` of
+dimensions ``(M, N)``, i.e., it returns two such matrices, ``q``\ ’, and
+``r``, that ``m = qr``, where ``q`` is orthonormal, and ``r`` is upper
+triangular. In addition to the input matrix, which is the first
+positional argument, the function accepts the ``mode`` keyword argument
+with a default value of ``reduced``. If ``mode`` is ``reduced``, ``q``,
+and ``r`` are returned in the reduced representation. Otherwise, the
+outputs will have dimensions ``(M, M)``, and ``(M, N)``, respectively.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ A = np.arange(6).reshape((3, 2))
+ print('A: \n', A)
+
+ print('complete decomposition')
+ q, r = np.linalg.qr(A, mode='complete')
+ print('q: \n', q)
+ print()
+ print('r: \n', r)
+
+ print('\n\nreduced decomposition')
+ q, r = np.linalg.qr(A, mode='reduced')
+ print('q: \n', q)
+ print()
+ print('r: \n', r)
+
+.. parsed-literal::
+
+ A:
+ array([[0, 1],
+ [2, 3],
+ [4, 5]], dtype=int16)
+ complete decomposition
+ q:
+ array([[0.0, -0.9128709291752768, 0.408248290463863],
+ [-0.447213595499958, -0.3651483716701107, -0.8164965809277261],
+ [-0.8944271909999159, 0.1825741858350553, 0.408248290463863]], dtype=float64)
+
+ r:
+ array([[-4.47213595499958, -5.813776741499454],
+ [0.0, -1.095445115010332],
+ [0.0, 0.0]], dtype=float64)
+
+
+ reduced decomposition
+ q:
+ array([[0.0, -0.9128709291752768],
+ [-0.447213595499958, -0.3651483716701107],
+ [-0.8944271909999159, 0.1825741858350553]], dtype=float64)
+
+ r:
+ array([[-4.47213595499958, -5.813776741499454],
+ [0.0, -1.095445115010332]], dtype=float64)
+
+
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/numpy-universal.rst b/circuitpython/extmod/ulab/docs/manual/source/numpy-universal.rst
new file mode 100644
index 0000000..b9b7f9f
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/numpy-universal.rst
@@ -0,0 +1,487 @@
+
+Universal functions
+===================
+
+Standard mathematical functions can be calculated on any scalar,
+scalar-valued iterable (ranges, lists, tuples containing numbers), and
+on ``ndarray``\ s without having to change the call signature. In all
+cases the functions return a new ``ndarray`` of typecode ``float``
+(since these functions usually generate float values, anyway). The only
+exceptions to this rule are the ``exp``, and ``sqrt`` functions, which,
+if ``ULAB_SUPPORTS_COMPLEX`` is set to 1 in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__,
+can return complex arrays, depending on the argument. All functions
+execute faster with ``ndarray`` arguments than with iterables, because
+the values of the input vector can be extracted faster.
+
+At present, the following functions are supported (starred functions can
+operate on, or can return complex arrays):
+
+``acos``, ``acosh``, ``arctan2``, ``around``, ``asin``, ``asinh``,
+``atan``, ``arctan2``, ``atanh``, ``ceil``, ``cos``, ``degrees``,
+``exp*``, ``expm1``, ``floor``, ``log``, ``log10``, ``log2``,
+``radians``, ``sin``, ``sinh``, ``sqrt*``, ``tan``, ``tanh``.
+
+These functions are applied element-wise to the arguments, thus, e.g.,
+the exponential of a matrix cannot be calculated in this way, only the
+exponential of the matrix entries.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = range(9)
+ b = np.array(a)
+
+ # works with ranges, lists, tuples etc.
+ print('a:\t', a)
+ print('exp(a):\t', np.exp(a))
+
+ # with 1D arrays
+ print('\n=============\nb:\n', b)
+ print('exp(b):\n', np.exp(b))
+
+ # as well as with matrices
+ c = np.array(range(9)).reshape((3, 3))
+ print('\n=============\nc:\n', c)
+ print('exp(c):\n', np.exp(c))
+
+.. parsed-literal::
+
+ a: range(0, 9)
+ exp(a): array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)
+
+ =============
+ b:
+ array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+ exp(b):
+ array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)
+
+ =============
+ c:
+ array([[0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ [6.0, 7.0, 8.0]], dtype=float64)
+ exp(c):
+ array([[1.0, 2.718281828459045, 7.38905609893065],
+ [20.08553692318767, 54.59815003314424, 148.4131591025766],
+ [403.4287934927351, 1096.633158428459, 2980.957987041728]], dtype=float64)
+
+
+
+
+Computation expenses
+--------------------
+
+The overhead for calculating with micropython iterables is quite
+significant: for the 1000 samples below, the difference is more than 800
+microseconds, because internally the function has to create the
+``ndarray`` for the output, has to fetch the iterable’s items of unknown
+type, and then convert them to floats. All these steps are skipped for
+``ndarray``\ s, because these pieces of information are already known.
+
+Doing the same with ``list`` comprehension requires 30 times more time
+than with the ``ndarray``, which would become even more, if we converted
+the resulting list to an ``ndarray``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ import math
+
+ a = [0]*1000
+ b = np.array(a)
+
+ @timeit
+ def timed_vector(iterable):
+ return np.exp(iterable)
+
+ @timeit
+ def timed_list(iterable):
+ return [math.exp(i) for i in iterable]
+
+ print('iterating over ndarray in ulab')
+ timed_vector(b)
+
+ print('\niterating over list in ulab')
+ timed_vector(a)
+
+ print('\niterating over list in python')
+ timed_list(a)
+
+.. parsed-literal::
+
+ iterating over ndarray in ulab
+ execution time: 441 us
+
+ iterating over list in ulab
+ execution time: 1266 us
+
+ iterating over list in python
+ execution time: 11379 us
+
+
+
+arctan2
+-------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.arctan2.html
+
+The two-argument inverse tangent function is also part of the ``vector``
+sub-module. The function implements broadcasting as discussed in the
+section on ``ndarray``\ s. Scalars (``micropython`` integers or floats)
+are also allowed.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2.2, 33.33, 444.444])
+ print('a:\n', a)
+ print('\narctan2(a, 1.0)\n', np.arctan2(a, 1.0))
+ print('\narctan2(1.0, a)\n', np.arctan2(1.0, a))
+ print('\narctan2(a, a): \n', np.arctan2(a, a))
+
+.. parsed-literal::
+
+ a:
+ array([1.0, 2.2, 33.33, 444.444], dtype=float64)
+
+ arctan2(a, 1.0)
+ array([0.7853981633974483, 1.14416883366802, 1.5408023243361, 1.568546328341769], dtype=float64)
+
+ arctan2(1.0, a)
+ array([0.7853981633974483, 0.426627493126876, 0.02999400245879636, 0.002249998453127392], dtype=float64)
+
+ arctan2(a, a):
+ array([0.7853981633974483, 0.7853981633974483, 0.7853981633974483, 0.7853981633974483], dtype=float64)
+
+
+
+
+around
+------
+
+``numpy``:
+https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.around.html
+
+``numpy``\ ’s ``around`` function can also be found in the ``vector``
+sub-module. The function implements the ``decimals`` keyword argument
+with default value ``0``. The first argument must be an ``ndarray``. If
+this is not the case, the function raises a ``TypeError`` exception.
+Note that ``numpy`` accepts general iterables. The ``out`` keyword
+argument known from ``numpy`` is not accepted. The function always
+returns an ndarray of type ``mp_float_t``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2.2, 33.33, 444.444])
+ print('a:\t\t', a)
+ print('\ndecimals = 0\t', np.around(a, decimals=0))
+ print('\ndecimals = 1\t', np.around(a, decimals=1))
+ print('\ndecimals = -1\t', np.around(a, decimals=-1))
+
+.. parsed-literal::
+
+ a: array([1.0, 2.2, 33.33, 444.444], dtype=float64)
+
+ decimals = 0 array([1.0, 2.0, 33.0, 444.0], dtype=float64)
+
+ decimals = 1 array([1.0, 2.2, 33.3, 444.4], dtype=float64)
+
+ decimals = -1 array([0.0, 0.0, 30.0, 440.0], dtype=float64)
+
+
+
+
+exp
+---
+
+If ``ULAB_SUPPORTS_COMPLEX`` is set to 1 in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__,
+the exponential function can also take complex arrays as its argument,
+in which case the return value is also complex.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3])
+ print('a:\t\t', a)
+ print('exp(a):\t\t', np.exp(a))
+ print()
+
+ b = np.array([1+1j, 2+2j, 3+3j], dtype=np.complex)
+ print('b:\t\t', b)
+ print('exp(b):\t\t', np.exp(b))
+
+.. parsed-literal::
+
+ a: array([1.0, 2.0, 3.0], dtype=float64)
+ exp(a): array([2.718281828459045, 7.38905609893065, 20.08553692318767], dtype=float64)
+
+ b: array([1.0+1.0j, 2.0+2.0j, 3.0+3.0j], dtype=complex)
+ exp(b): array([1.468693939915885+2.287355287178842j, -3.074932320639359+6.71884969742825j, -19.88453084414699+2.834471132487004j], dtype=complex)
+
+
+
+
+sqrt
+----
+
+If ``ULAB_SUPPORTS_COMPLEX`` is set to 1 in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__,
+the exponential function can also take complex arrays as its argument,
+in which case the return value is also complex. If the input is real,
+but the results might be complex, the user is supposed to specify the
+output ``dtype`` in the function call. Otherwise, the square roots of
+negative numbers will result in ``NaN``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, -1])
+ print('a:\t\t', a)
+ print('sqrt(a):\t\t', np.sqrt(a))
+ print('sqrt(a):\t\t', np.sqrt(a, dtype=np.complex))
+
+.. parsed-literal::
+
+ a: array([1.0, -1.0], dtype=float64)
+ sqrt(a): array([1.0, nan], dtype=float64)
+ sqrt(a): array([1.0+0.0j, 0.0+1.0j], dtype=complex)
+
+
+
+
+Vectorising generic python functions
+------------------------------------
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html
+
+The examples above use factory functions. In fact, they are nothing but
+the vectorised versions of the standard mathematical functions.
+User-defined ``python`` functions can also be vectorised by help of
+``vectorize``. This function takes a positional argument, namely, the
+``python`` function that you want to vectorise, and a non-mandatory
+keyword argument, ``otypes``, which determines the ``dtype`` of the
+output array. The ``otypes`` must be ``None`` (default), or any of the
+``dtypes`` defined in ``ulab``. With ``None``, the output is
+automatically turned into a float array.
+
+The return value of ``vectorize`` is a ``micropython`` object that can
+be called as a standard function, but which now accepts either a scalar,
+an ``ndarray``, or a generic ``micropython`` iterable as its sole
+argument. Note that the function that is to be vectorised must have a
+single argument.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ def f(x):
+ return x*x
+
+ vf = np.vectorize(f)
+
+ # calling with a scalar
+ print('{:20}'.format('f on a scalar: '), vf(44.0))
+
+ # calling with an ndarray
+ a = np.array([1, 2, 3, 4])
+ print('{:20}'.format('f on an ndarray: '), vf(a))
+
+ # calling with a list
+ print('{:20}'.format('f on a list: '), vf([2, 3, 4]))
+
+.. parsed-literal::
+
+ f on a scalar: array([1936.0], dtype=float64)
+ f on an ndarray: array([1.0, 4.0, 9.0, 16.0], dtype=float64)
+ f on a list: array([4.0, 9.0, 16.0], dtype=float64)
+
+
+
+
+As mentioned, the ``dtype`` of the resulting ``ndarray`` can be
+specified via the ``otypes`` keyword. The value is bound to the function
+object that ``vectorize`` returns, therefore, if the same function is to
+be vectorised with different output types, then for each type a new
+function object must be created.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ l = [1, 2, 3, 4]
+ def f(x):
+ return x*x
+
+ vf1 = np.vectorize(f, otypes=np.uint8)
+ vf2 = np.vectorize(f, otypes=np.float)
+
+ print('{:20}'.format('output is uint8: '), vf1(l))
+ print('{:20}'.format('output is float: '), vf2(l))
+
+.. parsed-literal::
+
+ output is uint8: array([1, 4, 9, 16], dtype=uint8)
+ output is float: array([1.0, 4.0, 9.0, 16.0], dtype=float64)
+
+
+
+
+The ``otypes`` keyword argument cannot be used for type coercion: if the
+function evaluates to a float, but ``otypes`` would dictate an integer
+type, an exception will be raised:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ int_list = [1, 2, 3, 4]
+ float_list = [1.0, 2.0, 3.0, 4.0]
+ def f(x):
+ return x*x
+
+ vf = np.vectorize(f, otypes=np.uint8)
+
+ print('{:20}'.format('integer list: '), vf(int_list))
+ # this will raise a TypeError exception
+ print(vf(float_list))
+
+.. parsed-literal::
+
+ integer list: array([1, 4, 9, 16], dtype=uint8)
+
+ Traceback (most recent call last):
+ File "/dev/shm/micropython.py", line 14, in <module>
+ TypeError: can't convert float to int
+
+
+
+Benchmarks
+~~~~~~~~~~
+
+It should be pointed out that the ``vectorize`` function produces the
+pseudo-vectorised version of the ``python`` function that is fed into
+it, i.e., on the C level, the same ``python`` function is called, with
+the all-encompassing ``mp_obj_t`` type arguments, and all that happens
+is that the ``for`` loop in ``[f(i) for i in iterable]`` runs purely in
+C. Since type checking and type conversion in ``f()`` is expensive, the
+speed-up is not so spectacular as when iterating over an ``ndarray``
+with a factory function: a gain of approximately 30% can be expected,
+when a native ``python`` type (e.g., ``list``) is returned by the
+function, and this becomes around 50% (a factor of 2), if conversion to
+an ``ndarray`` is also counted.
+
+The following code snippet calculates the square of a 1000 numbers with
+the vectorised function (which returns an ``ndarray``), with ``list``
+comprehension, and with ``list`` comprehension followed by conversion to
+an ``ndarray``. For comparison, the execution time is measured also for
+the case, when the square is calculated entirely in ``ulab``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ def f(x):
+ return x*x
+
+ vf = np.vectorize(f)
+
+ @timeit
+ def timed_vectorised_square(iterable):
+ return vf(iterable)
+
+ @timeit
+ def timed_python_square(iterable):
+ return [f(i) for i in iterable]
+
+ @timeit
+ def timed_ndarray_square(iterable):
+ return np.array([f(i) for i in iterable])
+
+ @timeit
+ def timed_ulab_square(ndarray):
+ return ndarray**2
+
+ print('vectorised function')
+ squares = timed_vectorised_square(range(1000))
+
+ print('\nlist comprehension')
+ squares = timed_python_square(range(1000))
+
+ print('\nlist comprehension + ndarray conversion')
+ squares = timed_ndarray_square(range(1000))
+
+ print('\nsquaring an ndarray entirely in ulab')
+ a = np.array(range(1000))
+ squares = timed_ulab_square(a)
+
+.. parsed-literal::
+
+ vectorised function
+ execution time: 7237 us
+
+ list comprehension
+ execution time: 10248 us
+
+ list comprehension + ndarray conversion
+ execution time: 12562 us
+
+ squaring an ndarray entirely in ulab
+ execution time: 560 us
+
+
+
+From the comparisons above, it is obvious that ``python`` functions
+should only be vectorised, when the same effect cannot be gotten in
+``ulab`` only. However, although the time savings are not significant,
+there is still a good reason for caring about vectorised functions.
+Namely, user-defined ``python`` functions become universal, i.e., they
+can accept generic iterables as well as ``ndarray``\ s as their
+arguments. A vectorised function is still a one-liner, resulting in
+transparent and elegant code.
+
+A final comment on this subject: the ``f(x)`` that we defined is a
+*generic* ``python`` function. This means that it is not required that
+it just crunches some numbers. It has to return a number object, but it
+can still access the hardware in the meantime. So, e.g.,
+
+.. code:: python
+
+
+ led = pyb.LED(2)
+
+ def f(x):
+ if x < 100:
+ led.toggle()
+ return x*x
+
+is perfectly valid code.
diff --git a/circuitpython/extmod/ulab/docs/manual/source/scipy-linalg.rst b/circuitpython/extmod/ulab/docs/manual/source/scipy-linalg.rst
new file mode 100644
index 0000000..5259682
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/scipy-linalg.rst
@@ -0,0 +1,151 @@
+
+scipy.linalg
+============
+
+``scipy``\ ’s ``linalg`` module contains two functions,
+``solve_triangular``, and ``cho_solve``. The functions can be called by
+prepending them by ``scipy.linalg.``.
+
+1. `scipy.linalg.solve_cho <#cho_solve>`__
+2. `scipy.linalg.solve_triangular <#solve_triangular>`__
+
+cho_solve
+---------
+
+``scipy``:
+https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.cho_solve.html
+
+Solve the linear equations
+
+:raw-latex:`\begin{equation}
+\mathbf{A}\cdot\mathbf{x} = \mathbf{b}
+\end{equation}`
+
+given the Cholesky factorization of :math:`\mathbf{A}`. As opposed to
+``scipy``, the function simply takes the Cholesky-factorised matrix,
+:math:`\mathbf{A}`, and :math:`\mathbf{b}` as inputs.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ A = np.array([[3, 0, 0, 0], [2, 1, 0, 0], [1, 0, 1, 0], [1, 2, 1, 8]])
+ b = np.array([4, 2, 4, 2])
+
+ print(spy.linalg.cho_solve(A, b))
+
+.. parsed-literal::
+
+ array([-0.01388888888888906, -0.6458333333333331, 2.677083333333333, -0.01041666666666667], dtype=float64)
+
+
+
+
+solve_triangular
+----------------
+
+``scipy``:
+https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve_triangular.html
+
+Solve the linear equation
+
+:raw-latex:`\begin{equation}
+\mathbf{a}\cdot\mathbf{x} = \mathbf{b}
+\end{equation}`
+
+with the assumption that :math:`\mathbf{a}` is a triangular matrix. The
+two position arguments are :math:`\mathbf{a}`, and :math:`\mathbf{b}`,
+and the optional keyword argument is ``lower`` with a default value of
+``False``. ``lower`` determines, whether data are taken from the lower,
+or upper triangle of :math:`\mathbf{a}`.
+
+Note that :math:`\mathbf{a}` itself does not have to be a triangular
+matrix: if it is not, then the values are simply taken to be 0 in the
+upper or lower triangle, as dictated by ``lower``. However,
+:math:`\mathbf{a}\cdot\mathbf{x}` will yield :math:`\mathbf{b}` only,
+when :math:`\mathbf{a}` is triangular. You should keep this in mind,
+when trying to establish the validity of the solution by back
+substitution.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ a = np.array([[3, 0, 0, 0], [2, 1, 0, 0], [1, 0, 1, 0], [1, 2, 1, 8]])
+ b = np.array([4, 2, 4, 2])
+
+ print('a:\n')
+ print(a)
+ print('\nb: ', b)
+
+ x = spy.linalg.solve_triangular(a, b, lower=True)
+
+ print('='*20)
+ print('x: ', x)
+ print('\ndot(a, x): ', np.dot(a, x))
+
+.. parsed-literal::
+
+ a:
+
+ array([[3.0, 0.0, 0.0, 0.0],
+ [2.0, 1.0, 0.0, 0.0],
+ [1.0, 0.0, 1.0, 0.0],
+ [1.0, 2.0, 1.0, 8.0]], dtype=float64)
+
+ b: array([4.0, 2.0, 4.0, 2.0], dtype=float64)
+ ====================
+ x: array([1.333333333333333, -0.6666666666666665, 2.666666666666667, -0.08333333333333337], dtype=float64)
+
+ dot(a, x): array([4.0, 2.0, 4.0, 2.0], dtype=float64)
+
+
+
+
+With get the same solution, :math:`\mathbf{x}`, with the following
+matrix, but the dot product of :math:`\mathbf{a}`, and
+:math:`\mathbf{x}` is no longer :math:`\mathbf{b}`:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ a = np.array([[3, 2, 1, 0], [2, 1, 0, 1], [1, 0, 1, 4], [1, 2, 1, 8]])
+ b = np.array([4, 2, 4, 2])
+
+ print('a:\n')
+ print(a)
+ print('\nb: ', b)
+
+ x = spy.linalg.solve_triangular(a, b, lower=True)
+
+ print('='*20)
+ print('x: ', x)
+ print('\ndot(a, x): ', np.dot(a, x))
+
+.. parsed-literal::
+
+ a:
+
+ array([[3.0, 2.0, 1.0, 0.0],
+ [2.0, 1.0, 0.0, 1.0],
+ [1.0, 0.0, 1.0, 4.0],
+ [1.0, 2.0, 1.0, 8.0]], dtype=float64)
+
+ b: array([4.0, 2.0, 4.0, 2.0], dtype=float64)
+ ====================
+ x: array([1.333333333333333, -0.6666666666666665, 2.666666666666667, -0.08333333333333337], dtype=float64)
+
+ dot(a, x): array([5.333333333333334, 1.916666666666666, 3.666666666666667, 2.0], dtype=float64)
+
+
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/scipy-optimize.rst b/circuitpython/extmod/ulab/docs/manual/source/scipy-optimize.rst
new file mode 100644
index 0000000..63d60dd
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/scipy-optimize.rst
@@ -0,0 +1,173 @@
+
+scipy.optimize
+==============
+
+Functions in the ``optimize`` module can be called by prepending them by
+``scipy.optimize.``. The module defines the following three functions:
+
+1. `scipy.optimize.bisect <#bisect>`__
+2. `scipy.optimize.fmin <#fmin>`__
+3. `scipy.optimize.newton <#newton>`__
+
+Note that routines that work with user-defined functions still have to
+call the underlying ``python`` code, and therefore, gains in speed are
+not as significant as with other vectorised operations. As a rule of
+thumb, a factor of two can be expected, when compared to an optimised
+``python`` implementation.
+
+bisect
+------
+
+``scipy``:
+https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.bisect.html
+
+``bisect`` finds the root of a function of one variable using a simple
+bisection routine. It takes three positional arguments, the function
+itself, and two starting points. The function must have opposite signs
+at the starting points. Returned is the position of the root.
+
+Two keyword arguments, ``xtol``, and ``maxiter`` can be supplied to
+control the accuracy, and the number of bisections, respectively.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import scipy as spy
+
+ def f(x):
+ return x*x - 1
+
+ print(spy.optimize.bisect(f, 0, 4))
+
+ print('only 8 bisections: ', spy.optimize.bisect(f, 0, 4, maxiter=8))
+
+ print('with 0.1 accuracy: ', spy.optimize.bisect(f, 0, 4, xtol=0.1))
+
+.. parsed-literal::
+
+ 0.9999997615814209
+ only 8 bisections: 0.984375
+ with 0.1 accuracy: 0.9375
+
+
+
+
+Performance
+~~~~~~~~~~~
+
+Since the ``bisect`` routine calls user-defined ``python`` functions,
+the speed gain is only about a factor of two, if compared to a purely
+``python`` implementation.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import scipy as spy
+
+ def f(x):
+ return (x-1)*(x-1) - 2.0
+
+ def bisect(f, a, b, xtol=2.4e-7, maxiter=100):
+ if f(a) * f(b) > 0:
+ raise ValueError
+
+ rtb = a if f(a) < 0.0 else b
+ dx = b - a if f(a) < 0.0 else a - b
+ for i in range(maxiter):
+ dx *= 0.5
+ x_mid = rtb + dx
+ mid_value = f(x_mid)
+ if mid_value < 0:
+ rtb = x_mid
+ if abs(dx) < xtol:
+ break
+
+ return rtb
+
+ @timeit
+ def bisect_scipy(f, a, b):
+ return spy.optimize.bisect(f, a, b)
+
+ @timeit
+ def bisect_timed(f, a, b):
+ return bisect(f, a, b)
+
+ print('bisect running in python')
+ bisect_timed(f, 3, 2)
+
+ print('bisect running in C')
+ bisect_scipy(f, 3, 2)
+
+.. parsed-literal::
+
+ bisect running in python
+ execution time: 1270 us
+ bisect running in C
+ execution time: 642 us
+
+
+
+fmin
+----
+
+``scipy``:
+https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin.html
+
+The ``fmin`` function finds the position of the minimum of a
+user-defined function by using the downhill simplex method. Requires two
+positional arguments, the function, and the initial value. Three keyword
+arguments, ``xatol``, ``fatol``, and ``maxiter`` stipulate conditions
+for stopping.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import scipy as spy
+
+ def f(x):
+ return (x-1)**2 - 1
+
+ print(spy.optimize.fmin(f, 3.0))
+ print(spy.optimize.fmin(f, 3.0, xatol=0.1))
+
+.. parsed-literal::
+
+ 0.9996093749999952
+ 1.199999999999996
+
+
+
+
+newton
+------
+
+``scipy``:https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton.html
+
+``newton`` finds a zero of a real, user-defined function using the
+Newton-Raphson (or secant or Halley’s) method. The routine requires two
+positional arguments, the function, and the initial value. Three keyword
+arguments can be supplied to control the iteration. These are the
+absolute and relative tolerances ``tol``, and ``rtol``, respectively,
+and the number of iterations before stopping, ``maxiter``. The function
+retuns a single scalar, the position of the root.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import scipy as spy
+
+ def f(x):
+ return x*x*x - 2.0
+
+ print(spy.optimize.newton(f, 3., tol=0.001, rtol=0.01))
+
+.. parsed-literal::
+
+ 1.260135727246117
+
+
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/scipy-signal.rst b/circuitpython/extmod/ulab/docs/manual/source/scipy-signal.rst
new file mode 100644
index 0000000..b3bcd52
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/scipy-signal.rst
@@ -0,0 +1,137 @@
+
+scipy.signal
+============
+
+Functions in the ``signal`` module can be called by prepending them by
+``scipy.signal.``. The module defines the following two functions:
+
+1. `scipy.signal.sosfilt <#sosfilt>`__
+2. `scipy.signal.spectrogram <#spectrogram>`__
+
+sosfilt
+-------
+
+``scipy``:
+https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.sosfilt.html
+
+Filter data along one dimension using cascaded second-order sections.
+
+The function takes two positional arguments, ``sos``, the filter
+segments of length 6, and the one-dimensional, uniformly sampled data
+set to be filtered. Returns the filtered data, or the filtered data and
+the final filter delays, if the ``zi`` keyword arguments is supplied.
+The keyword argument must be a float ``ndarray`` of shape
+``(n_sections, 2)``. If ``zi`` is not passed to the function, the
+initial values are assumed to be 0.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+ sos = [[1, 2, 3, 1, 5, 6], [1, 2, 3, 1, 5, 6]]
+ y = spy.signal.sosfilt(sos, x)
+ print('y: ', y)
+
+.. parsed-literal::
+
+ y: array([0.0, 1.0, -4.0, 24.0, -104.0, 440.0, -1728.0, 6532.000000000001, -23848.0, 84864.0], dtype=float)
+
+
+
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+ sos = [[1, 2, 3, 1, 5, 6], [1, 2, 3, 1, 5, 6]]
+ # initial conditions of the filter
+ zi = np.array([[1, 2], [3, 4]])
+
+ y, zf = spy.signal.sosfilt(sos, x, zi=zi)
+ print('y: ', y)
+ print('\n' + '='*40 + '\nzf: ', zf)
+
+.. parsed-literal::
+
+ y: array([4.0, -16.0, 63.00000000000001, -227.0, 802.9999999999999, -2751.0, 9271.000000000001, -30775.0, 101067.0, -328991.0000000001], dtype=float)
+
+ ========================================
+ zf: array([[37242.0, 74835.0],
+ [1026187.0, 1936542.0]], dtype=float)
+
+
+
+
+spectrogram
+-----------
+
+In addition to the Fourier transform and its inverse, ``ulab`` also
+sports a function called ``spectrogram``, which returns the absolute
+value of the Fourier transform. This could be used to find the dominant
+spectral component in a time series. The arguments are treated in the
+same way as in ``fft``, and ``ifft``. This means that, if the firmware
+was compiled with complex support, the input can also be a complex
+array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ x = np.linspace(0, 10, num=1024)
+ y = np.sin(x)
+
+ a = spy.signal.spectrogram(y)
+
+ print('original vector:\t', y)
+ print('\nspectrum:\t', a)
+
+.. parsed-literal::
+
+ original vector: array([0.0, 0.009775015390171337, 0.01954909674625918, ..., -0.5275140569487312, -0.5357931822978732, -0.5440211108893639], dtype=float64)
+
+ spectrum: array([187.8635087634579, 315.3112063607119, 347.8814873399374, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)
+
+
+
+
+As such, ``spectrogram`` is really just a shorthand for
+``np.sqrt(a*a + b*b)``:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ x = np.linspace(0, 10, num=1024)
+ y = np.sin(x)
+
+ a, b = np.fft.fft(y)
+
+ print('\nspectrum calculated the hard way:\t', np.sqrt(a*a + b*b))
+
+ a = spy.signal.spectrogram(y)
+
+ print('\nspectrum calculated the lazy way:\t', a)
+
+.. parsed-literal::
+
+
+ spectrum calculated the hard way: array([187.8635087634579, 315.3112063607119, 347.8814873399374, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)
+
+ spectrum calculated the lazy way: array([187.8635087634579, 315.3112063607119, 347.8814873399374, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)
+
+
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/scipy-special.rst b/circuitpython/extmod/ulab/docs/manual/source/scipy-special.rst
new file mode 100644
index 0000000..755a535
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/scipy-special.rst
@@ -0,0 +1,44 @@
+
+scipy.special
+=============
+
+``scipy``\ ’s ``special`` module defines several functions that behave
+as do the standard mathematical functions of the ``numpy``, i.e., they
+can be called on any scalar, scalar-valued iterable (ranges, lists,
+tuples containing numbers), and on ``ndarray``\ s without having to
+change the call signature. In all cases the functions return a new
+``ndarray`` of typecode ``float`` (since these functions usually
+generate float values, anyway).
+
+At present, ``ulab``\ ’s ``special`` module contains the following
+functions:
+
+``erf``, ``erfc``, ``gamma``, and ``gammaln``, and they can be called by
+prepending them by ``scipy.special.``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+ a = range(9)
+ b = np.array(a)
+
+ print('a: ', a)
+ print(spy.special.erf(a))
+
+ print('\nb: ', b)
+ print(spy.special.erfc(b))
+
+.. parsed-literal::
+
+ a: range(0, 9)
+ array([0.0, 0.8427007929497149, 0.9953222650189527, 0.9999779095030014, 0.9999999845827421, 1.0, 1.0, 1.0, 1.0], dtype=float64)
+
+ b: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+ array([1.0, 0.1572992070502851, 0.004677734981047265, 2.209049699858544e-05, 1.541725790028002e-08, 1.537459794428035e-12, 2.151973671249892e-17, 4.183825607779414e-23, 1.122429717298293e-29], dtype=float64)
+
+
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/ulab-intro.rst b/circuitpython/extmod/ulab/docs/manual/source/ulab-intro.rst
new file mode 100644
index 0000000..81019e3
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/ulab-intro.rst
@@ -0,0 +1,624 @@
+
+Introduction
+============
+
+Enter ulab
+----------
+
+``ulab`` is a ``numpy``-like module for ``micropython`` and its
+derivatives, meant to simplify and speed up common mathematical
+operations on arrays. ``ulab`` implements a small subset of ``numpy``
+and ``scipy``. The functions were chosen such that they might be useful
+in the context of a microcontroller. However, the project is a living
+one, and suggestions for new features are always welcome.
+
+This document discusses how you can use the library, starting from
+building your own firmware, through questions like what affects the
+firmware size, what are the trade-offs, and what are the most important
+differences to ``numpy`` and ``scipy``, respectively. The document is
+organised as follows:
+
+The chapter after this one helps you with firmware customisation.
+
+The third chapter gives a very concise summary of the ``ulab`` functions
+and array methods. This chapter can be used as a quick reference.
+
+The chapters after that are an in-depth review of most functions. Here
+you can find usage examples, benchmarks, as well as a thorough
+discussion of such concepts as broadcasting, and views versus copies.
+
+The final chapter of this book can be regarded as the programming
+manual. The inner working of ``ulab`` is dissected here, and you will
+also find hints as to how to implement your own ``numpy``-compatible
+functions.
+
+Purpose
+-------
+
+Of course, the first question that one has to answer is, why on Earth
+one would need a fast math library on a microcontroller. After all, it
+is not expected that heavy number crunching is going to take place on
+bare metal. It is not meant to. On a PC, the main reason for writing
+fast code is the sheer amount of data that one wants to process. On a
+microcontroller, the data volume is probably small, but it might lead to
+catastrophic system failure, if these data are not processed in time,
+because the microcontroller is supposed to interact with the outside
+world in a timely fashion. In fact, this latter objective was the
+initiator of this project: I needed the Fourier transform of a signal
+coming from the ADC of the ``pyboard``, and all available options were
+simply too slow.
+
+In addition to speed, another issue that one has to keep in mind when
+working with embedded systems is the amount of available RAM: I believe,
+everything here could be implemented in pure ``python`` with relatively
+little effort (in fact, there are a couple of ``python``-only
+implementations of ``numpy`` functions out there), but the price we
+would have to pay for that is not only speed, but RAM, too. ``python``
+code, if is not frozen, and compiled into the firmware, has to be
+compiled at runtime, which is not exactly a cheap process. On top of
+that, if numbers are stored in a list or tuple, which would be the
+high-level container, then they occupy 8 bytes, no matter, whether they
+are all smaller than 100, or larger than one hundred million. This is
+obviously a waste of resources in an environment, where resources are
+scarce.
+
+Finally, there is a reason for using ``micropython`` in the first place.
+Namely, that a microcontroller can be programmed in a very elegant, and
+*pythonic* way. But if it is so, why should we not extend this idea to
+other tasks and concepts that might come up in this context? If there
+was no other reason than this *elegance*, I would find that convincing
+enough.
+
+Based on the above-mentioned considerations, all functions in ``ulab``
+are implemented in a way that
+
+1. conforms to ``numpy`` as much as possible
+2. is so frugal with RAM as possible,
+3. and yet, fast. Much faster than pure python. Think of speed-ups of
+ 30-50!
+
+The main points of ``ulab`` are
+
+- compact, iterable and slicable containers of numerical data in one to
+ four dimensions. These containers support all the relevant unary and
+ binary operators (e.g., ``len``, ==, +, \*, etc.)
+- vectorised computations on ``micropython`` iterables and numerical
+ arrays (in ``numpy``-speak, universal functions)
+- computing statistical properties (mean, standard deviation etc.) on
+ arrays
+- basic linear algebra routines (matrix inversion, multiplication,
+ reshaping, transposition, determinant, and eigenvalues, Cholesky
+ decomposition and so on)
+- polynomial fits to numerical data, and evaluation of polynomials
+- fast Fourier transforms
+- filtering of data (convolution and second-order filters)
+- function minimisation, fitting, and numerical approximation routines
+- interfacing between numerical data and peripheral hardware devices
+
+``ulab`` implements close to a hundred functions and array methods. At
+the time of writing this manual (for version 4.0.0), the library adds
+approximately 120 kB of extra compiled code to the ``micropython``
+(pyboard.v.1.17) firmware. However, if you are tight with flash space,
+you can easily shave tens of kB off the firmware. In fact, if only a
+small sub-set of functions are needed, you can get away with less than
+10 kB of flash space. See the section on `customising
+ulab <#Customising-the-firmware>`__.
+
+Resources and legal matters
+---------------------------
+
+The source code of the module can be found under
+https://github.com/v923z/micropython-ulab/tree/master/code. while the
+source of this user manual is under
+https://github.com/v923z/micropython-ulab/tree/master/docs.
+
+The MIT licence applies to all material.
+
+Friendly request
+----------------
+
+If you use ``ulab``, and bump into a bug, or think that a particular
+function is missing, or its behaviour does not conform to ``numpy``,
+please, raise a `ulab
+issue <#https://github.com/v923z/micropython-ulab/issues>`__ on github,
+so that the community can profit from your experiences.
+
+Even better, if you find the project to be useful, and think that it
+could be made better, faster, tighter, and shinier, please, consider
+contributing, and issue a pull request with the implementation of your
+improvements and new features. ``ulab`` can only become successful, if
+it offers what the community needs.
+
+These last comments apply to the documentation, too. If, in your
+opinion, the documentation is obscure, misleading, or not detailed
+enough, please, let us know, so that *we* can fix it.
+
+Differences between micropython-ulab and circuitpython-ulab
+-----------------------------------------------------------
+
+``ulab`` has originally been developed for ``micropython``, but has
+since been integrated into a number of its flavours. Most of these are
+simply forks of ``micropython`` itself, with some additional
+functionality. One of the notable exceptions is ``circuitpython``, which
+has slightly diverged at the core level, and this has some minor
+consequences. Some of these concern the C implementation details only,
+which all have been sorted out with the generous and enthusiastic
+support of Jeff Epler from `Adafruit
+Industries <http://www.adafruit.com>`__.
+
+There are, however, a couple of instances, where the two environments
+differ at the python level in how the class properties can be accessed.
+We will point out the differences and possible workarounds at the
+relevant places in this document.
+
+Customising the firmware
+========================
+
+As mentioned above, ``ulab`` has considerably grown since its
+conception, which also means that it might no longer fit on the
+microcontroller of your choice. There are, however, a couple of ways of
+customising the firmware, and thereby reducing its size.
+
+All ``ulab`` options are listed in a single header file,
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__,
+which contains pre-processor flags for each feature that can be
+fine-tuned. The first couple of lines of the file look like this
+
+.. code:: c
+
+ // The pre-processor constants in this file determine how ulab behaves:
+ //
+ // - how many dimensions ulab can handle
+ // - which functions are included in the compiled firmware
+ // - whether the python syntax is numpy-like, or modular
+ // - whether arrays can be sliced and iterated over
+ // - which binary/unary operators are supported
+ //
+ // A considerable amount of flash space can be saved by removing (setting
+ // the corresponding constants to 0) the unnecessary functions and features.
+
+ // Values defined here can be overridden by your own config file as
+ // make -DULAB_CONFIG_FILE="my_ulab_config.h"
+ #if defined(ULAB_CONFIG_FILE)
+ #include ULAB_CONFIG_FILE
+ #endif
+
+ // Adds support for complex ndarrays
+ #ifndef ULAB_SUPPORTS_COMPLEX
+ #define ULAB_SUPPORTS_COMPLEX (1)
+ #endif
+
+ // Determines, whether scipy is defined in ulab. The sub-modules and functions
+ // of scipy have to be defined separately
+ #define ULAB_HAS_SCIPY (1)
+
+ // The maximum number of dimensions the firmware should be able to support
+ // Possible values lie between 1, and 4, inclusive
+ #define ULAB_MAX_DIMS 2
+
+ // By setting this constant to 1, iteration over array dimensions will be implemented
+ // as a function (ndarray_rewind_array), instead of writing out the loops in macros
+ // This reduces firmware size at the expense of speed
+ #define ULAB_HAS_FUNCTION_ITERATOR (0)
+
+ // If NDARRAY_IS_ITERABLE is 1, the ndarray object defines its own iterator function
+ // This option saves approx. 250 bytes of flash space
+ #define NDARRAY_IS_ITERABLE (1)
+
+ // Slicing can be switched off by setting this variable to 0
+ #define NDARRAY_IS_SLICEABLE (1)
+
+ // The default threshold for pretty printing. These variables can be overwritten
+ // at run-time via the set_printoptions() function
+ #define ULAB_HAS_PRINTOPTIONS (1)
+ #define NDARRAY_PRINT_THRESHOLD 10
+ #define NDARRAY_PRINT_EDGEITEMS 3
+
+ // determines, whether the dtype is an object, or simply a character
+ // the object implementation is numpythonic, but requires more space
+ #define ULAB_HAS_DTYPE_OBJECT (0)
+
+ // the ndarray binary operators
+ #define NDARRAY_HAS_BINARY_OPS (1)
+
+ // Firmware size can be reduced at the expense of speed by using function
+ // pointers in iterations. For each operator, he function pointer saves around
+ // 2 kB in the two-dimensional case, and around 4 kB in the four-dimensional case.
+
+ #define NDARRAY_BINARY_USES_FUN_POINTER (0)
+
+ #define NDARRAY_HAS_BINARY_OP_ADD (1)
+ #define NDARRAY_HAS_BINARY_OP_EQUAL (1)
+ #define NDARRAY_HAS_BINARY_OP_LESS (1)
+ #define NDARRAY_HAS_BINARY_OP_LESS_EQUAL (1)
+ #define NDARRAY_HAS_BINARY_OP_MORE (1)
+ #define NDARRAY_HAS_BINARY_OP_MORE_EQUAL (1)
+ #define NDARRAY_HAS_BINARY_OP_MULTIPLY (1)
+ #define NDARRAY_HAS_BINARY_OP_NOT_EQUAL (1)
+ #define NDARRAY_HAS_BINARY_OP_POWER (1)
+ #define NDARRAY_HAS_BINARY_OP_SUBTRACT (1)
+ #define NDARRAY_HAS_BINARY_OP_TRUE_DIVIDE (1)
+ ...
+
+The meaning of flags with names ``_HAS_`` should be obvious, so we will
+just explain the other options.
+
+To see how much you can gain by un-setting the functions that you do not
+need, here are some pointers. In four dimensions, including all
+functions adds around 120 kB to the ``micropython`` firmware. On the
+other hand, if you are interested in Fourier transforms only, and strip
+everything else, you get away with less than 5 kB extra.
+
+Compatibility with numpy
+------------------------
+
+The functions implemented in ``ulab`` are organised in four sub-modules
+at the C level, namely, ``numpy``, ``scipy``, ``utils``, and ``user``.
+This modularity is elevated to ``python``, meaning that in order to use
+functions that are part of ``numpy``, you have to import ``numpy`` as
+
+.. code:: python
+
+ from ulab import numpy as np
+
+ x = np.array([4, 5, 6])
+ p = np.array([1, 2, 3])
+ np.polyval(p, x)
+
+There are a couple of exceptions to this rule, namely ``fft``, and
+``linalg``, which are sub-modules even in ``numpy``, thus you have to
+write them out as
+
+.. code:: python
+
+ from ulab import numpy as np
+
+ A = np.array([1, 2, 3, 4]).reshape()
+ np.linalg.trace(A)
+
+Some of the functions in ``ulab`` are re-implementations of ``scipy``
+functions, and they are to be imported as
+
+.. code:: python
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+
+ x = np.array([1, 2, 3])
+ spy.special.erf(x)
+
+``numpy``-compatibility has an enormous benefit : namely, by
+``try``\ ing to ``import``, we can guarantee that the same, unmodified
+code runs in ``CPython``, as in ``micropython``. The following snippet
+is platform-independent, thus, the ``python`` code can be tested and
+debugged on a computer before loading it onto the microcontroller.
+
+.. code:: python
+
+
+ try:
+ from ulab import numpy as np
+ from ulab import scipy as spy
+ except ImportError:
+ import numpy as np
+ import scipy as spy
+
+ x = np.array([1, 2, 3])
+ spy.special.erf(x)
+
+The impact of dimensionality
+----------------------------
+
+Reducing the number of dimensions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``ulab`` supports tensors of rank four, but this is expensive in terms
+of flash: with all available functions and options, the library adds
+around 100 kB to the firmware. However, if such high dimensions are not
+required, significant reductions in size can be gotten by changing the
+value of
+
+.. code:: c
+
+ #define ULAB_MAX_DIMS 2
+
+Two dimensions cost a bit more than half of four, while you can get away
+with around 20 kB of flash in one dimension, because all those functions
+that don’t make sense (e.g., matrix inversion, eigenvalues etc.) are
+automatically stripped from the firmware.
+
+Using the function iterator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In higher dimensions, the firmware size increases, because each
+dimension (axis) adds another level of nested loops. An example of this
+is the macro of the binary operator in three dimensions
+
+.. code:: c
+
+ #define BINARY_LOOP(results, type_out, type_left, type_right, larray, lstrides, rarray, rstrides, OPERATOR)
+ type_out *array = (type_out *)results->array;
+ size_t j = 0;
+ do {
+ size_t k = 0;
+ do {
+ size_t l = 0;
+ do {
+ *array++ = *((type_left *)(larray)) OPERATOR *((type_right *)(rarray));
+ (larray) += (lstrides)[ULAB_MAX_DIMS - 1];
+ (rarray) += (rstrides)[ULAB_MAX_DIMS - 1];
+ l++;
+ } while(l < (results)->shape[ULAB_MAX_DIMS - 1]);
+ (larray) -= (lstrides)[ULAB_MAX_DIMS - 1] * (results)->shape[ULAB_MAX_DIMS-1];
+ (larray) += (lstrides)[ULAB_MAX_DIMS - 2];
+ (rarray) -= (rstrides)[ULAB_MAX_DIMS - 1] * (results)->shape[ULAB_MAX_DIMS-1];
+ (rarray) += (rstrides)[ULAB_MAX_DIMS - 2];
+ k++;
+ } while(k < (results)->shape[ULAB_MAX_DIMS - 2]);
+ (larray) -= (lstrides)[ULAB_MAX_DIMS - 2] * results->shape[ULAB_MAX_DIMS-2];
+ (larray) += (lstrides)[ULAB_MAX_DIMS - 3];
+ (rarray) -= (rstrides)[ULAB_MAX_DIMS - 2] * results->shape[ULAB_MAX_DIMS-2];
+ (rarray) += (rstrides)[ULAB_MAX_DIMS - 3];
+ j++;
+ } while(j < (results)->shape[ULAB_MAX_DIMS - 3]);
+
+In order to reduce firmware size, it *might* make sense in higher
+dimensions to make use of the function iterator by setting the
+
+.. code:: c
+
+ #define ULAB_HAS_FUNCTION_ITERATOR (1)
+
+constant to 1. This allows the compiler to call the
+``ndarray_rewind_array`` function, so that it doesn’t have to unwrap the
+loops for ``k``, and ``j``. Instead of the macro above, we now have
+
+.. code:: c
+
+ #define BINARY_LOOP(results, type_out, type_left, type_right, larray, lstrides, rarray, rstrides, OPERATOR)
+ type_out *array = (type_out *)(results)->array;
+ size_t *lcoords = ndarray_new_coords((results)->ndim);
+ size_t *rcoords = ndarray_new_coords((results)->ndim);
+ for(size_t i=0; i < (results)->len/(results)->shape[ULAB_MAX_DIMS -1]; i++) {
+ size_t l = 0;
+ do {
+ *array++ = *((type_left *)(larray)) OPERATOR *((type_right *)(rarray));
+ (larray) += (lstrides)[ULAB_MAX_DIMS - 1];
+ (rarray) += (rstrides)[ULAB_MAX_DIMS - 1];
+ l++;
+ } while(l < (results)->shape[ULAB_MAX_DIMS - 1]);
+ ndarray_rewind_array((results)->ndim, larray, (results)->shape, lstrides, lcoords);
+ ndarray_rewind_array((results)->ndim, rarray, (results)->shape, rstrides, rcoords);
+ } while(0)
+
+Since the ``ndarray_rewind_array`` function is implemented only once, a
+lot of space can be saved. Obviously, function calls cost time, thus
+such trade-offs must be evaluated for each application. The gain also
+depends on which functions and features you include. Operators and
+functions that involve two arrays are expensive, because at the C level,
+the number of cases that must be handled scales with the squares of the
+number of data types. As an example, the innocent-looking expression
+
+.. code:: python
+
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3])
+ b = np.array([4, 5, 6])
+
+ c = a + b
+
+requires 25 loops in C, because the ``dtypes`` of both ``a``, and ``b``
+can assume 5 different values, and the addition has to be resolved for
+all possible cases. Hint: each binary operator costs between 3 and 4 kB
+in two dimensions.
+
+The ulab version string
+-----------------------
+
+As is customary with ``python`` packages, information on the package
+version can be found be querying the ``__version__`` string.
+
+.. code::
+
+ # code to be run in micropython
+
+ import ulab
+
+ print('you are running ulab version', ulab.__version__)
+
+.. parsed-literal::
+
+ you are running ulab version 2.1.0-2D
+
+
+
+
+The first three numbers indicate the major, minor, and sub-minor
+versions of ``ulab`` (defined by the ``ULAB_VERSION`` constant in
+`ulab.c <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.c>`__).
+We usually change the minor version, whenever a new function is added to
+the code, and the sub-minor version will be incremented, if a bug fix is
+implemented.
+
+``2D`` tells us that the particular firmware supports tensors of rank 2
+(defined by ``ULAB_MAX_DIMS`` in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__).
+
+If you find a bug, please, include the version string in your report!
+
+Should you need the numerical value of ``ULAB_MAX_DIMS``, you can get it
+from the version string in the following way:
+
+.. code::
+
+ # code to be run in micropython
+
+ import ulab
+
+ version = ulab.__version__
+ version_dims = version.split('-')[1]
+ version_num = int(version_dims.replace('D', ''))
+
+ print('version string: ', version)
+ print('version dimensions: ', version_dims)
+ print('numerical value of dimensions: ', version_num)
+
+.. parsed-literal::
+
+ version string: 2.1.0-2D
+ version dimensions: 2D
+ numerical value of dimensions: 2
+
+
+
+
+ulab with complex arrays
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the firmware supports complex arrays, ``-c`` is appended to the
+version string as can be seen below.
+
+.. code::
+
+ # code to be run in micropython
+
+ import ulab
+
+ version = ulab.__version__
+
+ print('version string: ', version)
+
+.. parsed-literal::
+
+ version string: 4.0.0-2D-c
+
+
+
+
+Finding out what your firmware supports
+---------------------------------------
+
+``ulab`` implements a number of array operators and functions, but this
+does not mean that all of these functions and methods are actually
+compiled into the firmware. You can fine-tune your firmware by
+setting/unsetting any of the ``_HAS_`` constants in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__.
+
+Functions included in the firmware
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The version string will not tell you everything about your firmware,
+because the supported functions and sub-modules can still arbitrarily be
+included or excluded. One way of finding out what is compiled into the
+firmware is calling ``dir`` with ``ulab`` as its argument.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import scipy as spy
+
+
+ print('===== constants, functions, and modules of numpy =====\n\n', dir(np))
+
+ # since fft and linalg are sub-modules, print them separately
+ print('\nfunctions included in the fft module:\n', dir(np.fft))
+ print('\nfunctions included in the linalg module:\n', dir(np.linalg))
+
+ print('\n\n===== modules of scipy =====\n\n', dir(spy))
+ print('\nfunctions included in the optimize module:\n', dir(spy.optimize))
+ print('\nfunctions included in the signal module:\n', dir(spy.signal))
+ print('\nfunctions included in the special module:\n', dir(spy.special))
+
+.. parsed-literal::
+
+ ===== constants, functions, and modules of numpy =====
+
+ ['__class__', '__name__', 'bool', 'sort', 'sum', 'acos', 'acosh', 'arange', 'arctan2', 'argmax', 'argmin', 'argsort', 'around', 'array', 'asin', 'asinh', 'atan', 'atanh', 'ceil', 'clip', 'concatenate', 'convolve', 'cos', 'cosh', 'cross', 'degrees', 'diag', 'diff', 'e', 'equal', 'exp', 'expm1', 'eye', 'fft', 'flip', 'float', 'floor', 'frombuffer', 'full', 'get_printoptions', 'inf', 'int16', 'int8', 'interp', 'linalg', 'linspace', 'log', 'log10', 'log2', 'logspace', 'max', 'maximum', 'mean', 'median', 'min', 'minimum', 'nan', 'ndinfo', 'not_equal', 'ones', 'pi', 'polyfit', 'polyval', 'radians', 'roll', 'set_printoptions', 'sin', 'sinh', 'sqrt', 'std', 'tan', 'tanh', 'trapz', 'uint16', 'uint8', 'vectorize', 'zeros']
+
+ functions included in the fft module:
+ ['__class__', '__name__', 'fft', 'ifft']
+
+ functions included in the linalg module:
+ ['__class__', '__name__', 'cholesky', 'det', 'dot', 'eig', 'inv', 'norm', 'trace']
+
+
+ ===== modules of scipy =====
+
+ ['__class__', '__name__', 'optimize', 'signal', 'special']
+
+ functions included in the optimize module:
+ ['__class__', '__name__', 'bisect', 'fmin', 'newton']
+
+ functions included in the signal module:
+ ['__class__', '__name__', 'sosfilt', 'spectrogram']
+
+ functions included in the special module:
+ ['__class__', '__name__', 'erf', 'erfc', 'gamma', 'gammaln']
+
+
+
+
+Methods included in the firmware
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``dir`` function applied to the module or its sub-modules gives
+information on what the module and sub-modules include, but is not
+enough to find out which methods the ``ndarray`` class supports. We can
+list the methods by calling ``dir`` with the ``array`` object itself:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print(dir(np.array))
+
+.. parsed-literal::
+
+ ['__class__', '__name__', 'copy', 'sort', '__bases__', '__dict__', 'dtype', 'flatten', 'itemsize', 'reshape', 'shape', 'size', 'strides', 'tobytes', 'transpose']
+
+
+
+
+Operators included in the firmware
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A list of operators cannot be generated as shown above. If you really
+need to find out, whether, e.g., the ``**`` operator is supported by the
+firmware, you have to ``try`` it:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3])
+ b = np.array([4, 5, 6])
+
+ try:
+ print(a ** b)
+ except Exception as e:
+ print('operator is not supported: ', e)
+
+.. parsed-literal::
+
+ operator is not supported: unsupported types for __pow__: 'ndarray', 'ndarray'
+
+
+
+
+The exception above would be raised, if the firmware was compiled with
+the
+
+.. code:: c
+
+ #define NDARRAY_HAS_BINARY_OP_POWER (0)
+
+definition.
diff --git a/circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst b/circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst
new file mode 100644
index 0000000..a37cef7
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst
@@ -0,0 +1,2607 @@
+
+ndarray, the base class
+=======================
+
+The ``ndarray`` is the underlying container of numerical data. It can be
+thought of as micropython’s own ``array`` object, but has a great number
+of extra features starting with how it can be initialised, which
+operations can be done on it, and which functions can accept it as an
+argument. One important property of an ``ndarray`` is that it is also a
+proper ``micropython`` iterable.
+
+The ``ndarray`` consists of a short header, and a pointer that holds the
+data. The pointer always points to a contiguous segment in memory
+(``numpy`` is more flexible in this regard), and the header tells the
+interpreter, how the data from this segment is to be read out, and what
+the bytes mean. Some operations, e.g., ``reshape``, are fast, because
+they do not operate on the data, they work on the header, and therefore,
+only a couple of bytes are manipulated, even if there are a million data
+entries. A more detailed exposition of how operators are implemented can
+be found in the section titled `Programming ulab <#Programming_ula>`__.
+
+Since the ``ndarray`` is a binary container, it is also compact, meaning
+that it takes only a couple of bytes of extra RAM in addition to what is
+required for storing the numbers themselves. ``ndarray``\ s are also
+type-aware, i.e., one can save RAM by specifying a data type, and using
+the smallest reasonable one. Five such types are defined, namely
+``uint8``, ``int8``, which occupy a single byte of memory per datum,
+``uint16``, and ``int16``, which occupy two bytes per datum, and
+``float``, which occupies four or eight bytes per datum. The
+precision/size of the ``float`` type depends on the definition of
+``mp_float_t``. Some platforms, e.g., the PYBD, implement ``double``\ s,
+but some, e.g., the pyboard.v.11, do not. You can find out, what type of
+float your particular platform implements by looking at the output of
+the `.itemsize <#.itemsize>`__ class property, or looking at the exact
+``dtype``, when you print out an array.
+
+In addition to the five above-mentioned numerical types, it is also
+possible to define Boolean arrays, which can be used in the indexing of
+data. However, Boolean arrays are really nothing but arrays of type
+``uint8`` with an extra flag.
+
+On the following pages, we will see how one can work with
+``ndarray``\ s. Those familiar with ``numpy`` should find that the
+nomenclature and naming conventions of ``numpy`` are adhered to as
+closely as possible. We will point out the few differences, where
+necessary.
+
+For the sake of comparison, in addition to the ``ulab`` code snippets,
+sometimes the equivalent ``numpy`` code is also presented. You can find
+out, where the snippet is supposed to run by looking at its first line,
+the header of the code block.
+
+The ndinfo function
+-------------------
+
+A concise summary of a couple of the properties of an ``ndarray`` can be
+printed out by calling the ``ndinfo`` function. In addition to finding
+out what the *shape* and *strides* of the array array, we also get the
+``itemsize``, as well as the type. An interesting piece of information
+is the *data pointer*, which tells us, what the address of the data
+segment of the ``ndarray`` is. We will see the significance of this in
+the section `Slicing and indexing <#Slicing-and-indexing>`__.
+
+Note that this function simply prints some information, but does not
+return anything. If you need to get a handle of the data contained in
+the printout, you should call the dedicated ``shape``, ``strides``, or
+``itemsize`` functions directly.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(5), dtype=np.float)
+ b = np.array(range(25), dtype=np.uint8).reshape((5, 5))
+ np.ndinfo(a)
+ print('\n')
+ np.ndinfo(b)
+
+.. parsed-literal::
+
+ class: ndarray
+ shape: (5,)
+ strides: (8,)
+ itemsize: 8
+ data pointer: 0x7f8f6fa2e240
+ type: float
+
+
+ class: ndarray
+ shape: (5, 5)
+ strides: (5, 1)
+ itemsize: 1
+ data pointer: 0x7f8f6fa2e2e0
+ type: uint8
+
+
+
+
+Initialising an array
+---------------------
+
+A new array can be created by passing either a standard micropython
+iterable, or another ``ndarray`` into the constructor.
+
+Initialising by passing iterables
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the iterable is one-dimensional, i.e., one whose elements are
+numbers, then a row vector will be created and returned. If the iterable
+is two-dimensional, i.e., one whose elements are again iterables, a
+matrix will be created. If the lengths of the iterables are not
+consistent, a ``ValueError`` will be raised. Iterables of different
+types can be mixed in the initialisation function.
+
+If the ``dtype`` keyword with the possible
+``uint8/int8/uint16/int16/float`` values is supplied, the new
+``ndarray`` will have that type, otherwise, it assumes ``float`` as
+default. In addition, if ``ULAB_SUPPORTS_COMPLEX`` is set to 1 in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__,
+the ``dtype`` can also take on the value of ``complex``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = [1, 2, 3, 4, 5, 6, 7, 8]
+ b = np.array(a)
+
+ print("a:\t", a)
+ print("b:\t", b)
+
+ # a two-dimensional array with mixed-type initialisers
+ c = np.array([range(5), range(20, 25, 1), [44, 55, 66, 77, 88]], dtype=np.uint8)
+ print("\nc:\t", c)
+
+ # and now we throw an exception
+ d = np.array([range(5), range(10), [44, 55, 66, 77, 88]], dtype=np.uint8)
+ print("\nd:\t", d)
+
+.. parsed-literal::
+
+ a: [1, 2, 3, 4, 5, 6, 7, 8]
+ b: array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+
+ c: array([[0, 1, 2, 3, 4],
+ [20, 21, 22, 23, 24],
+ [44, 55, 66, 77, 88]], dtype=uint8)
+
+ Traceback (most recent call last):
+ File "/dev/shm/micropython.py", line 15, in <module>
+ ValueError: iterables are not of the same length
+
+
+
+Initialising by passing arrays
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An ``ndarray`` can be initialised by supplying another array. This
+statement is almost trivial, since ``ndarray``\ s are iterables
+themselves, though it should be pointed out that initialising through
+arrays is a bit faster. This statement is especially true, if the
+``dtype``\ s of the source and output arrays are the same, because then
+the contents can simply be copied without further ado. While type
+conversion is also possible, it will always be slower than straight
+copying.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = [1, 2, 3, 4, 5, 6, 7, 8]
+ b = np.array(a)
+ c = np.array(b)
+ d = np.array(b, dtype=np.uint8)
+
+ print("a:\t", a)
+ print("\nb:\t", b)
+ print("\nc:\t", c)
+ print("\nd:\t", d)
+
+.. parsed-literal::
+
+ a: [1, 2, 3, 4, 5, 6, 7, 8]
+
+ b: array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+
+ c: array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)
+
+ d: array([1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)
+
+
+
+
+Note that the default type of the ``ndarray`` is ``float``. Hence, if
+the array is initialised from another array, type conversion will always
+take place, except, when the output type is specifically supplied. I.e.,
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(5), dtype=np.uint8)
+ b = np.array(a)
+ print("a:\t", a)
+ print("\nb:\t", b)
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4], dtype=uint8)
+
+ b: array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float64)
+
+
+
+
+will iterate over the elements in ``a``, since in the assignment
+``b = np.array(a)``, no output type was given, therefore, ``float`` was
+assumed. On the other hand,
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(5), dtype=np.uint8)
+ b = np.array(a, dtype=np.uint8)
+ print("a:\t", a)
+ print("\nb:\t", b)
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4], dtype=uint8)
+
+ b: array([0, 1, 2, 3, 4], dtype=uint8)
+
+
+
+
+will simply copy the content of ``a`` into ``b`` without any iteration,
+and will, therefore, be faster. Keep this in mind, whenever the output
+type, or performance is important.
+
+Array initialisation functions
+------------------------------
+
+There are nine functions that can be used for initialising an array.
+Starred functions accept ``complex`` as the value of the ``dtype``, if
+the firmware was compiled with complex support.
+
+1. `numpy.arange <#arange>`__
+2. `numpy.concatenate <#concatenate>`__
+3. `numpy.diag\* <#diag>`__
+4. `numpy.empty\* <#empty>`__
+5. `numpy.eye\* <#eye>`__
+6. `numpy.frombuffer <#frombuffer>`__
+7. `numpy.full\* <#full>`__
+8. `numpy.linspace\* <#linspace>`__
+9. `numpy.logspace <#logspace>`__
+10. `numpy.ones\* <#ones>`__
+11. `numpy.zeros\* <#zeros>`__
+
+arange
+~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.arange.html
+
+The function returns a one-dimensional array with evenly spaced values.
+Takes 3 positional arguments (two are optional), and the ``dtype``
+keyword argument.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print(np.arange(10))
+ print(np.arange(2, 10))
+ print(np.arange(2, 10, 3))
+ print(np.arange(2, 10, 3, dtype=np.float))
+
+.. parsed-literal::
+
+ array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)
+ array([2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)
+ array([2, 5, 8], dtype=int16)
+ array([2.0, 5.0, 8.0], dtype=float64)
+
+
+
+
+concatenate
+~~~~~~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.concatenate.html
+
+The function joins a sequence of arrays, if they are compatible in
+shape, i.e., if all shapes except the one along the joining axis are
+equal.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(25), dtype=np.uint8).reshape((5, 5))
+ b = np.array(range(15), dtype=np.uint8).reshape((3, 5))
+
+ c = np.concatenate((a, b), axis=0)
+ print(c)
+
+.. parsed-literal::
+
+ array([[0, 1, 2, 3, 4],
+ [5, 6, 7, 8, 9],
+ [10, 11, 12, 13, 14],
+ [15, 16, 17, 18, 19],
+ [20, 21, 22, 23, 24],
+ [0, 1, 2, 3, 4],
+ [5, 6, 7, 8, 9],
+ [10, 11, 12, 13, 14]], dtype=uint8)
+
+
+
+
+**WARNING**: ``numpy`` accepts arbitrary ``dtype``\ s in the sequence of
+arrays, in ``ulab`` the ``dtype``\ s must be identical. If you want to
+concatenate different types, you have to convert all arrays to the same
+type first. Here ``b`` is of ``float`` type, so it cannot directly be
+concatenated to ``a``. However, if we cast the ``dtype`` of ``b``, the
+concatenation works:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(25), dtype=np.uint8).reshape((5, 5))
+ b = np.array(range(15), dtype=np.float).reshape((5, 3))
+ d = np.array(b+1, dtype=np.uint8)
+ print('a: ', a)
+ print('='*20 + '\nd: ', d)
+ c = np.concatenate((d, a), axis=1)
+ print('='*20 + '\nc: ', c)
+
+.. parsed-literal::
+
+ a: array([[0, 1, 2, 3, 4],
+ [5, 6, 7, 8, 9],
+ [10, 11, 12, 13, 14],
+ [15, 16, 17, 18, 19],
+ [20, 21, 22, 23, 24]], dtype=uint8)
+ ====================
+ d: array([[1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ [13, 14, 15]], dtype=uint8)
+ ====================
+ c: array([[1, 2, 3, 0, 1, 2, 3, 4],
+ [4, 5, 6, 5, 6, 7, 8, 9],
+ [7, 8, 9, 10, 11, 12, 13, 14],
+ [10, 11, 12, 15, 16, 17, 18, 19],
+ [13, 14, 15, 20, 21, 22, 23, 24]], dtype=uint8)
+
+
+
+
+diag
+----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.diag.html
+
+Extract a diagonal, or construct a diagonal array.
+
+The function takes two arguments, an ``ndarray``, and a shift. If the
+first argument is a two-dimensional array, the function returns a
+one-dimensional array containing the diagonal entries. The diagonal can
+be shifted by an amount given in the second argument.
+
+If the first argument is a one-dimensional array, the function returns a
+two-dimensional tensor with its diagonal elements given by the first
+argument.
+
+The ``diag`` function can accept a complex array, if the firmware was
+compiled with complex support.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4])
+ print(np.diag(a))
+
+.. parsed-literal::
+
+ array([[1.0, 0.0, 0.0, 0.0],
+ [0.0, 2.0, 0.0, 0.0],
+ [0.0, 0.0, 3.0, 0.0],
+ [0.0, 0.0, 0.0, 4.0]], dtype=float64)
+
+
+
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(16)).reshape((4, 4))
+ print('a: ', a)
+ print()
+ print('diagonal of a: ', np.diag(a))
+
+.. parsed-literal::
+
+ a: array([[0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0],
+ [12.0, 13.0, 14.0, 15.0]], dtype=float64)
+
+ diagonal of a: array([0.0, 5.0, 10.0, 15.0], dtype=float64)
+
+
+
+
+empty
+-----
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.empty.html
+
+``empty`` is simply an alias for ``zeros``, i.e., as opposed to
+``numpy``, the entries of the tensor will be initialised to zero.
+
+The ``empty`` function can accept complex as the value of the dtype, if
+the firmware was compiled with complex support.
+
+eye
+~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.eye.html
+
+Another special array method is the ``eye`` function, whose call
+signature is
+
+.. code:: python
+
+ eye(N, M, k=0, dtype=float)
+
+where ``N`` (``M``) specify the dimensions of the matrix (if only ``N``
+is supplied, then we get a square matrix, otherwise one with ``M`` rows,
+and ``N`` columns), and ``k`` is the shift of the ones (the main
+diagonal corresponds to ``k=0``). Here are a couple of examples.
+
+The ``eye`` function can accept ``complex`` as the value of the
+``dtype``, if the firmware was compiled with complex support.
+
+With a single argument
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print(np.eye(5))
+
+.. parsed-literal::
+
+ array([[1.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 1.0]], dtype=float64)
+
+
+
+
+Specifying the dimensions of the matrix
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print(np.eye(4, M=6, k=-1, dtype=np.int16))
+
+.. parsed-literal::
+
+ array([[0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0]], dtype=int16)
+
+
+
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print(np.eye(4, M=6, dtype=np.int8))
+
+.. parsed-literal::
+
+ array([[1, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0]], dtype=int8)
+
+
+
+
+frombuffer
+~~~~~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.frombuffer.html
+
+The function interprets a contiguous buffer as a one-dimensional array,
+and thus can be used for piping buffered data directly into an array.
+This method of analysing, e.g., ADC data is much more efficient than
+passing the ADC buffer into the ``array`` constructor, because
+``frombuffer`` simply creates the ``ndarray`` header and blindly copies
+the memory segment, without inspecting the underlying data.
+
+The function takes a single positional argument, the buffer, and three
+keyword arguments. These are the ``dtype`` with a default value of
+``float``, the ``offset``, with a default of 0, and the ``count``, with
+a default of -1, meaning that all data are taken in.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ buffer = b'\x01\x02\x03\x04\x05\x06\x07\x08'
+ print('buffer: ', buffer)
+
+ a = np.frombuffer(buffer, dtype=np.uint8)
+ print('a, all data read: ', a)
+
+ b = np.frombuffer(buffer, dtype=np.uint8, offset=2)
+ print('b, all data with an offset: ', b)
+
+ c = np.frombuffer(buffer, dtype=np.uint8, offset=2, count=3)
+ print('c, only 3 items with an offset: ', c)
+
+.. parsed-literal::
+
+ buffer: b'\x01\x02\x03\x04\x05\x06\x07\x08'
+ a, all data read: array([1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)
+ b, all data with an offset: array([3, 4, 5, 6, 7, 8], dtype=uint8)
+ c, only 3 items with an offset: array([3, 4, 5], dtype=uint8)
+
+
+
+
+full
+~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.full.html
+
+The function returns an array of arbitrary dimension, whose elements are
+all equal to the second positional argument. The first argument is a
+tuple describing the shape of the tensor. The ``dtype`` keyword argument
+with a default value of ``float`` can also be supplied.
+
+The ``full`` function can accept a complex scalar, or ``complex`` as the
+value of ``dtype``, if the firmware was compiled with complex support.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ # create an array with the default type
+ print(np.full((2, 4), 3))
+
+ print('\n' + '='*20 + '\n')
+ # the array type is uint8 now
+ print(np.full((2, 4), 3, dtype=np.uint8))
+
+.. parsed-literal::
+
+ array([[3.0, 3.0, 3.0, 3.0],
+ [3.0, 3.0, 3.0, 3.0]], dtype=float64)
+
+ ====================
+
+ array([[3, 3, 3, 3],
+ [3, 3, 3, 3]], dtype=uint8)
+
+
+
+
+linspace
+~~~~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html
+
+This function returns an array, whose elements are uniformly spaced
+between the ``start``, and ``stop`` points. The number of intervals is
+determined by the ``num`` keyword argument, whose default value is 50.
+With the ``endpoint`` keyword argument (defaults to ``True``) one can
+include ``stop`` in the sequence. In addition, the ``dtype`` keyword can
+be supplied to force type conversion of the output. The default is
+``float``. Note that, when ``dtype`` is of integer type, the sequence is
+not necessarily evenly spaced. This is not an error, rather a
+consequence of rounding. (This is also the ``numpy`` behaviour.)
+
+The ``linspace`` function can accept ``complex`` as the value of the
+``dtype``, if the firmware was compiled with complex support. The output
+``dtype`` is automatically complex, if either of the endpoints is a
+complex scalar.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ # generate a sequence with defaults
+ print('default sequence:\t', np.linspace(0, 10))
+
+ # num=5
+ print('num=5:\t\t\t', np.linspace(0, 10, num=5))
+
+ # num=5, endpoint=False
+ print('num=5:\t\t\t', np.linspace(0, 10, num=5, endpoint=False))
+
+ # num=5, endpoint=False, dtype=uint8
+ print('num=5:\t\t\t', np.linspace(0, 5, num=7, endpoint=False, dtype=np.uint8))
+
+.. parsed-literal::
+
+ default sequence: array([0.0, 0.2040816326530612, 0.4081632653061225, ..., 9.591836734693871, 9.795918367346932, 9.999999999999993], dtype=float64)
+ num=5: array([0.0, 2.5, 5.0, 7.5, 10.0], dtype=float64)
+ num=5: array([0.0, 2.0, 4.0, 6.0, 8.0], dtype=float64)
+ num=5: array([0, 0, 1, 2, 2, 3, 4], dtype=uint8)
+
+
+
+
+logspace
+~~~~~~~~
+
+``linspace``\ ’ equivalent for logarithmically spaced data is
+``logspace``. This function produces a sequence of numbers, in which the
+quotient of consecutive numbers is constant. This is a geometric
+sequence.
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.logspace.html
+
+This function returns an array, whose elements are uniformly spaced
+between the ``start``, and ``stop`` points. The number of intervals is
+determined by the ``num`` keyword argument, whose default value is 50.
+With the ``endpoint`` keyword argument (defaults to ``True``) one can
+include ``stop`` in the sequence. In addition, the ``dtype`` keyword can
+be supplied to force type conversion of the output. The default is
+``float``. Note that, exactly as in ``linspace``, when ``dtype`` is of
+integer type, the sequence is not necessarily evenly spaced in log
+space.
+
+In addition to the keyword arguments found in ``linspace``, ``logspace``
+also accepts the ``base`` argument. The default value is 10.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ # generate a sequence with defaults
+ print('default sequence:\t', np.logspace(0, 3))
+
+ # num=5
+ print('num=5:\t\t\t', np.logspace(1, 10, num=5))
+
+ # num=5, endpoint=False
+ print('num=5:\t\t\t', np.logspace(1, 10, num=5, endpoint=False))
+
+ # num=5, endpoint=False
+ print('num=5:\t\t\t', np.logspace(1, 10, num=5, endpoint=False, base=2))
+
+.. parsed-literal::
+
+ default sequence: array([1.0, 1.151395399326447, 1.325711365590109, ..., 754.3120063354646, 868.5113737513561, 1000.000000000004], dtype=float64)
+ num=5: array([10.0, 1778.279410038923, 316227.766016838, 56234132.5190349, 10000000000.0], dtype=float64)
+ num=5: array([10.0, 630.9573444801933, 39810.71705534974, 2511886.431509581, 158489319.2461114], dtype=float64)
+ num=5: array([2.0, 6.964404506368993, 24.25146506416637, 84.44850628946524, 294.066778879241], dtype=float64)
+
+
+
+
+ones, zeros
+~~~~~~~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.zeros.html
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.ones.html
+
+A couple of special arrays and matrices can easily be initialised by
+calling one of the ``ones``, or ``zeros`` functions. ``ones`` and
+``zeros`` follow the same pattern, and have the call signature
+
+.. code:: python
+
+ ones(shape, dtype=float)
+ zeros(shape, dtype=float)
+
+where shape is either an integer, or a tuple specifying the shape.
+
+The ``ones/zeros`` functions can accept complex as the value of the
+dtype, if the firmware was compiled with complex support.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ print(np.ones(6, dtype=np.uint8))
+
+ print(np.zeros((6, 4)))
+
+.. parsed-literal::
+
+ array([1, 1, 1, 1, 1, 1], dtype=uint8)
+ array([[0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0]], dtype=float64)
+
+
+
+
+When specifying the shape, make sure that the length of the tuple is not
+larger than the maximum dimension of your firmware.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ import ulab
+
+ print('maximum number of dimensions: ', ulab.__version__)
+
+ print(np.zeros((2, 2, 2)))
+
+.. parsed-literal::
+
+ maximum number of dimensions: 2.1.0-2D
+
+ Traceback (most recent call last):
+ File "/dev/shm/micropython.py", line 7, in <module>
+ TypeError: too many dimensions
+
+
+
+Customising array printouts
+---------------------------
+
+``ndarray``\ s are pretty-printed, i.e., if the number of entries along
+the last axis is larger than 10 (default value), then only the first and
+last three entries will be printed. Also note that, as opposed to
+``numpy``, the printout always contains the ``dtype``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(200))
+ print("a:\t", a)
+
+.. parsed-literal::
+
+ a: array([0.0, 1.0, 2.0, ..., 197.0, 198.0, 199.0], dtype=float64)
+
+
+
+
+set_printoptions
+~~~~~~~~~~~~~~~~
+
+The default values can be overwritten by means of the
+``set_printoptions`` function
+`numpy.set_printoptions <https://numpy.org/doc/1.18/reference/generated/numpy.set_printoptions.html>`__,
+which accepts two keywords arguments, the ``threshold``, and the
+``edgeitems``. The first of these arguments determines the length of the
+longest array that will be printed in full, while the second is the
+number of items that will be printed on the left and right hand side of
+the ellipsis, if the array is longer than ``threshold``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(20))
+ print("a printed with defaults:\t", a)
+
+ np.set_printoptions(threshold=200)
+ print("\na printed in full:\t\t", a)
+
+ np.set_printoptions(threshold=10, edgeitems=2)
+ print("\na truncated with 2 edgeitems:\t", a)
+
+.. parsed-literal::
+
+ a printed with defaults: array([0.0, 1.0, 2.0, ..., 17.0, 18.0, 19.0], dtype=float64)
+
+ a printed in full: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0], dtype=float64)
+
+ a truncated with 2 edgeitems: array([0.0, 1.0, ..., 18.0, 19.0], dtype=float64)
+
+
+
+
+get_printoptions
+~~~~~~~~~~~~~~~~
+
+The set value of the ``threshold`` and ``edgeitems`` can be retrieved by
+calling the ``get_printoptions`` function with no arguments. The
+function returns a *dictionary* with two keys.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ np.set_printoptions(threshold=100, edgeitems=20)
+ print(np.get_printoptions())
+
+.. parsed-literal::
+
+ {'threshold': 100, 'edgeitems': 20}
+
+
+
+
+Methods and properties of ndarrays
+----------------------------------
+
+Arrays have several *properties* that can queried, and some methods that
+can be called. With the exception of the flatten and transpose
+operators, properties return an object that describe some feature of the
+array, while the methods return a new array-like object. The ``imag``,
+and ``real`` properties are included in the firmware only, when it was
+compiled with complex support.
+
+1. `.byteswap <#.byteswap>`__
+2. `.copy <#.copy>`__
+3. `.dtype <#.dtype>`__
+4. `.flat <#.flat>`__
+5. `.flatten <#.flatten>`__
+6. `.imag\* <#.imag>`__
+7. `.itemsize <#.itemsize>`__
+8. `.real\* <#.real>`__
+9. `.reshape <#.reshape>`__
+10. `.shape <#.shape>`__
+11. `.size <#.size>`__
+12. `.T <#.transpose>`__
+13. `.tobytes <#.tobytes>`__
+14. `.tolist <#.tolist>`__
+15. `.transpose <#.transpose>`__
+16. `.sort <#.sort>`__
+
+.byteswap
+~~~~~~~~~
+
+``numpy``
+https://numpy.org/doc/stable/reference/generated/numpy.char.chararray.byteswap.html
+
+The method takes a single keyword argument, ``inplace``, with values
+``True`` or ``False``, and swaps the bytes in the array. If
+``inplace = False``, a new ``ndarray`` is returned, otherwise the
+original values are overwritten.
+
+The ``frombuffer`` function is a convenient way of receiving data from
+peripheral devices that work with buffers. However, it is not guaranteed
+that the byte order (in other words, the *endianness*) of the peripheral
+device matches that of the microcontroller. The ``.byteswap`` method
+makes it possible to change the endianness of the incoming data stream.
+
+Obviously, byteswapping makes sense only for those cases, when a datum
+occupies more than one byte, i.e., for the ``uint16``, ``int16``, and
+``float`` ``dtype``\ s. When ``dtype`` is either ``uint8``, or ``int8``,
+the method simply returns a view or copy of self, depending upon the
+value of ``inplace``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ buffer = b'\x01\x02\x03\x04\x05\x06\x07\x08'
+ print('buffer: ', buffer)
+
+ a = np.frombuffer(buffer, dtype=np.uint16)
+ print('a: ', a)
+ b = a.byteswap()
+ print('b: ', b)
+
+.. parsed-literal::
+
+ buffer: b'\x01\x02\x03\x04\x05\x06\x07\x08'
+ a: array([513, 1027, 1541, 2055], dtype=uint16)
+ b: array([258, 772, 1286, 1800], dtype=uint16)
+
+
+
+
+.copy
+~~~~~
+
+The ``.copy`` method creates a new *deep copy* of an array, i.e., the
+entries of the source array are *copied* into the target array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.int8)
+ b = a.copy()
+ print('a: ', a)
+ print('='*20)
+ print('b: ', b)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4], dtype=int8)
+ ====================
+ b: array([1, 2, 3, 4], dtype=int8)
+
+
+
+
+.dtype
+~~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.dtype.htm
+
+The ``.dtype`` property is the ``dtype`` of an array. This can then be
+used for initialising another array with the matching type. ``ulab``
+implements two versions of ``dtype``; one that is ``numpy``-like, i.e.,
+one, which returns a ``dtype`` object, and one that is significantly
+cheaper in terms of flash space, but does not define a ``dtype`` object,
+and holds a single character (number) instead.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.int8)
+ b = np.array([5, 6, 7], dtype=a.dtype)
+ print('a: ', a)
+ print('dtype of a: ', a.dtype)
+ print('\nb: ', b)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4], dtype=int8)
+ dtype of a: dtype('int8')
+
+ b: array([5, 6, 7], dtype=int8)
+
+
+
+
+If the ``ulab.h`` header file sets the pre-processor constant
+``ULAB_HAS_DTYPE_OBJECT`` to 0 as
+
+.. code:: c
+
+ #define ULAB_HAS_DTYPE_OBJECT (0)
+
+then the output of the previous snippet will be
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.int8)
+ b = np.array([5, 6, 7], dtype=a.dtype)
+ print('a: ', a)
+ print('dtype of a: ', a.dtype)
+ print('\nb: ', b)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4], dtype=int8)
+ dtype of a: 98
+
+ b: array([5, 6, 7], dtype=int8)
+
+
+
+
+Here 98 is nothing but the ASCII value of the character ``b``, which is
+the type code for signed 8-bit integers. The object definition adds
+around 600 bytes to the firmware.
+
+.flat
+~~~~~
+
+numpy:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flat.htm
+
+``.flat`` returns the array’s flat iterator. For one-dimensional objects
+the flat iterator is equivalent to the standart iterator, while for
+higher dimensional tensors, it amounts to first flattening the array,
+and then iterating over it. Note, however, that the flat iterator does
+not consume RAM beyond what is required for holding the position of the
+iterator itself, while flattening produces a new copy.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.int8)
+ for _a in a:
+ print(_a)
+
+ a = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.int8)
+ print('a:\n', a)
+
+ for _a in a:
+ print(_a)
+
+ for _a in a.flat:
+ print(_a)
+
+.. parsed-literal::
+
+ 1
+ 2
+ 3
+ 4
+ a:
+ array([[1, 2, 3, 4],
+ [5, 6, 7, 8]], dtype=int8)
+ array([1, 2, 3, 4], dtype=int8)
+ array([5, 6, 7, 8], dtype=int8)
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+
+
+
+
+.flatten
+~~~~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flatten.htm
+
+``.flatten`` returns the flattened array. The array can be flattened in
+``C`` style (i.e., moving along the last axis in the tensor), or in
+``fortran`` style (i.e., moving along the first axis in the tensor).
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.int8)
+ print("a: \t\t", a)
+ print("a flattened: \t", a.flatten())
+
+ b = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int8)
+ print("\nb:", b)
+
+ print("b flattened (C): \t", b.flatten())
+ print("b flattened (F): \t", b.flatten(order='F'))
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4], dtype=int8)
+ a flattened: array([1, 2, 3, 4], dtype=int8)
+
+ b: array([[1, 2, 3],
+ [4, 5, 6]], dtype=int8)
+ b flattened (C): array([1, 2, 3, 4, 5, 6], dtype=int8)
+ b flattened (F): array([1, 4, 2, 5, 3, 6], dtype=int8)
+
+
+
+
+.imag
+~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.imag.html
+
+The ``.imag`` property is defined only, if the firmware was compiled
+with complex support, and returns a copy with the imaginary part of an
+array. If the array is real, then the output is straight zeros with the
+``dtype`` of the input. If the input is complex, the output ``dtype`` is
+always ``float``, irrespective of the values.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3], dtype=np.uint16)
+ print("a:\t", a)
+ print("a.imag:\t", a.imag)
+
+ b = np.array([1, 2+1j, 3-1j], dtype=np.complex)
+ print("\nb:\t", b)
+ print("b.imag:\t", b.imag)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3], dtype=uint16)
+ a.imag: array([0, 0, 0], dtype=uint16)
+
+ b: array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)
+ b.imag: array([0.0, 1.0, -1.0], dtype=float64)
+
+
+
+
+.itemsize
+~~~~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.itemsize.html
+
+The ``.itemsize`` property is an integer with the size of elements in
+the array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3], dtype=np.int8)
+ print("a:\n", a)
+ print("itemsize of a:", a.itemsize)
+
+ b= np.array([[1, 2], [3, 4]], dtype=np.float)
+ print("\nb:\n", b)
+ print("itemsize of b:", b.itemsize)
+
+.. parsed-literal::
+
+ a:
+ array([1, 2, 3], dtype=int8)
+ itemsize of a: 1
+
+ b:
+ array([[1.0, 2.0],
+ [3.0, 4.0]], dtype=float64)
+ itemsize of b: 8
+
+
+
+
+.real
+~~~~~
+
+numpy:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.real.html
+
+The ``.real`` property is defined only, if the firmware was compiled
+with complex support, and returns a copy with the real part of an array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3], dtype=np.uint16)
+ print("a:\t", a)
+ print("a.real:\t", a.real)
+
+ b = np.array([1, 2+1j, 3-1j], dtype=np.complex)
+ print("\nb:\t", b)
+ print("b.real:\t", b.real)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3], dtype=uint16)
+ a.real: array([1, 2, 3], dtype=uint16)
+
+ b: array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)
+ b.real: array([1.0, 2.0, 3.0], dtype=float64)
+
+
+
+
+.reshape
+~~~~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html
+
+``reshape`` re-writes the shape properties of an ``ndarray``, but the
+array will not be modified in any other way. The function takes a single
+2-tuple with two integers as its argument. The 2-tuple should specify
+the desired number of rows and columns. If the new shape is not
+consistent with the old, a ``ValueError`` exception will be raised.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]], dtype=np.uint8)
+ print('a (4 by 4):', a)
+ print('a (2 by 8):', a.reshape((2, 8)))
+ print('a (1 by 16):', a.reshape((1, 16)))
+
+.. parsed-literal::
+
+ a (4 by 4): array([[1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16]], dtype=uint8)
+ a (2 by 8): array([[1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16]], dtype=uint8)
+ a (1 by 16): array([[1, 2, 3, ..., 14, 15, 16]], dtype=uint8)
+
+
+
+
+.. code::
+
+ # code to be run in CPython
+
+ Note that `ndarray.reshape()` can also be called by assigning to `ndarray.shape`.
+.shape
+~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.shape.html
+
+The ``.shape`` property is a tuple whose elements are the length of the
+array along each axis.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.int8)
+ print("a:\n", a)
+ print("shape of a:", a.shape)
+
+ b= np.array([[1, 2], [3, 4]], dtype=np.int8)
+ print("\nb:\n", b)
+ print("shape of b:", b.shape)
+
+.. parsed-literal::
+
+ a:
+ array([1, 2, 3, 4], dtype=int8)
+ shape of a: (4,)
+
+ b:
+ array([[1, 2],
+ [3, 4]], dtype=int8)
+ shape of b: (2, 2)
+
+
+
+
+By assigning a tuple to the ``.shape`` property, the array can be
+``reshape``\ d:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
+ print('a:\n', a)
+
+ a.shape = (3, 3)
+ print('\na:\n', a)
+
+.. parsed-literal::
+
+ a:
+ array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], dtype=float64)
+
+ a:
+ array([[1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0],
+ [7.0, 8.0, 9.0]], dtype=float64)
+
+
+
+
+.size
+~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.size.html
+
+The ``.size`` property is an integer specifying the number of elements
+in the array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3], dtype=np.int8)
+ print("a:\n", a)
+ print("size of a:", a.size)
+
+ b= np.array([[1, 2], [3, 4]], dtype=np.int8)
+ print("\nb:\n", b)
+ print("size of b:", b.size)
+
+.. parsed-literal::
+
+ a:
+ array([1, 2, 3], dtype=int8)
+ size of a: 3
+
+ b:
+ array([[1, 2],
+ [3, 4]], dtype=int8)
+ size of b: 4
+
+
+
+
+.T
+
+The ``.T`` property of the ``ndarray`` is equivalent to
+`.transpose <#.transpose>`__.
+
+.tobytes
+~~~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tobytes.html
+
+The ``.tobytes`` method can be used for acquiring a handle of the
+underlying data pointer of an array, and it returns a new ``bytearray``
+that can be fed into any method that can accep a ``bytearray``, e.g.,
+ADC data can be buffered into this ``bytearray``, or the ``bytearray``
+can be fed into a DAC. Since the ``bytearray`` is really nothing but the
+bare data container of the array, any manipulation on the ``bytearray``
+automatically modifies the array itself.
+
+Note that the method raises a ``ValueError`` exception, if the array is
+not dense (i.e., it has already been sliced).
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(8), dtype=np.uint8)
+ print('a: ', a)
+ b = a.tobytes()
+ print('b: ', b)
+
+ # modify b
+ b[0] = 13
+
+ print('='*20)
+ print('b: ', b)
+ print('a: ', a)
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)
+ b: bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07')
+ ====================
+ b: bytearray(b'\r\x01\x02\x03\x04\x05\x06\x07')
+ a: array([13, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)
+
+
+
+
+.tolist
+~~~~~~~
+
+``numpy``:
+https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tolist.html
+
+The ``.tolist`` method can be used for converting the numerical array
+into a (nested) ``python`` lists.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(4), dtype=np.uint8)
+ print('a: ', a)
+ b = a.tolist()
+ print('b: ', b)
+
+ c = a.reshape((2, 2))
+ print('='*20)
+ print('c: ', c)
+ d = c.tolist()
+ print('d: ', d)
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3], dtype=uint8)
+ b: [0, 1, 2, 3]
+ ====================
+ c: array([[0, 1],
+ [2, 3]], dtype=uint8)
+ d: [[0, 1], [2, 3]]
+
+
+
+
+.transpose
+~~~~~~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.transpose.html
+
+Returns the transposed array. Only defined, if the number of maximum
+dimensions is larger than 1.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=np.uint8)
+ print('a:\n', a)
+ print('shape of a:', a.shape)
+ a.transpose()
+ print('\ntranspose of a:\n', a)
+ print('shape of a:', a.shape)
+
+.. parsed-literal::
+
+ a:
+ array([[1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12]], dtype=uint8)
+ shape of a: (4, 3)
+
+ transpose of a:
+ array([[1, 4, 7, 10],
+ [2, 5, 8, 11],
+ [3, 6, 9, 12]], dtype=uint8)
+ shape of a: (3, 4)
+
+
+
+
+The transpose of the array can also be gotten through the ``T``
+property:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)
+ print('a:\n', a)
+ print('\ntranspose of a:\n', a.T)
+
+.. parsed-literal::
+
+ a:
+ array([[1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9]], dtype=uint8)
+
+ transpose of a:
+ array([[1, 4, 7],
+ [2, 5, 8],
+ [3, 6, 9]], dtype=uint8)
+
+
+
+
+.sort
+~~~~~
+
+``numpy``:
+https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html
+
+In-place sorting of an ``ndarray``. For a more detailed exposition, see
+`sort <#sort>`__.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)
+ print('\na:\n', a)
+ a.sort(axis=0)
+ print('\na sorted along vertical axis:\n', a)
+
+ a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)
+ a.sort(axis=1)
+ print('\na sorted along horizontal axis:\n', a)
+
+ a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)
+ a.sort(axis=None)
+ print('\nflattened a sorted:\n', a)
+
+.. parsed-literal::
+
+
+ a:
+ array([[1, 12, 3, 0],
+ [5, 3, 4, 1],
+ [9, 11, 1, 8],
+ [7, 10, 0, 1]], dtype=uint8)
+
+ a sorted along vertical axis:
+ array([[1, 3, 0, 0],
+ [5, 10, 1, 1],
+ [7, 11, 3, 1],
+ [9, 12, 4, 8]], dtype=uint8)
+
+ a sorted along horizontal axis:
+ array([[0, 1, 3, 12],
+ [1, 3, 4, 5],
+ [1, 8, 9, 11],
+ [0, 1, 7, 10]], dtype=uint8)
+
+ flattened a sorted:
+ array([0, 0, 1, ..., 10, 11, 12], dtype=uint8)
+
+
+
+
+Unary operators
+---------------
+
+With the exception of ``len``, which returns a single number, all unary
+operators manipulate the underlying data element-wise.
+
+len
+~~~
+
+This operator takes a single argument, the array, and returns either the
+length of the first axis.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)
+ b = np.array([range(5), range(5), range(5), range(5)], dtype=np.uint8)
+
+ print("a:\t", a)
+ print("length of a: ", len(a))
+ print("shape of a: ", a.shape)
+ print("\nb:\t", b)
+ print("length of b: ", len(b))
+ print("shape of b: ", b.shape)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4, 5], dtype=uint8)
+ length of a: 5
+ shape of a: (5,)
+
+ b: array([[0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4],
+ [0, 1, 2, 3, 4]], dtype=uint8)
+ length of b: 2
+ shape of b: (4, 5)
+
+
+
+
+The number returned by ``len`` is also the length of the iterations,
+when the array supplies the elements for an iteration (see later).
+
+invert
+~~~~~~
+
+The function is defined for integer data types (``uint8``, ``int8``,
+``uint16``, and ``int16``) only, takes a single argument, and returns
+the element-by-element, bit-wise inverse of the array. If a ``float`` is
+supplied, the function raises a ``ValueError`` exception.
+
+With signed integers (``int8``, and ``int16``), the results might be
+unexpected, as in the example below:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([0, -1, -100], dtype=np.int8)
+ print("a:\t\t", a)
+ print("inverse of a:\t", ~a)
+
+ a = np.array([0, 1, 254, 255], dtype=np.uint8)
+ print("\na:\t\t", a)
+ print("inverse of a:\t", ~a)
+
+.. parsed-literal::
+
+ a: array([0, -1, -100], dtype=int8)
+ inverse of a: array([-1, 0, 99], dtype=int8)
+
+ a: array([0, 1, 254, 255], dtype=uint8)
+ inverse of a: array([255, 254, 1, 0], dtype=uint8)
+
+
+
+
+abs
+~~~
+
+This function takes a single argument, and returns the
+element-by-element absolute value of the array. When the data type is
+unsigned (``uint8``, or ``uint16``), a copy of the array will be
+returned immediately, and no calculation takes place.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([0, -1, -100], dtype=np.int8)
+ print("a:\t\t\t ", a)
+ print("absolute value of a:\t ", abs(a))
+
+.. parsed-literal::
+
+ a: array([0, -1, -100], dtype=int8)
+ absolute value of a: array([0, 1, 100], dtype=int8)
+
+
+
+
+neg
+~~~
+
+This operator takes a single argument, and changes the sign of each
+element in the array. Unsigned values are wrapped.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([10, -1, 1], dtype=np.int8)
+ print("a:\t\t", a)
+ print("negative of a:\t", -a)
+
+ b = np.array([0, 100, 200], dtype=np.uint8)
+ print("\nb:\t\t", b)
+ print("negative of b:\t", -b)
+
+.. parsed-literal::
+
+ a: array([10, -1, 1], dtype=int8)
+ negative of a: array([-10, 1, -1], dtype=int8)
+
+ b: array([0, 100, 200], dtype=uint8)
+ negative of b: array([0, 156, 56], dtype=uint8)
+
+
+
+
+pos
+~~~
+
+This function takes a single argument, and simply returns a copy of the
+array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([10, -1, 1], dtype=np.int8)
+ print("a:\t\t", a)
+ print("positive of a:\t", +a)
+
+.. parsed-literal::
+
+ a: array([10, -1, 1], dtype=int8)
+ positive of a: array([10, -1, 1], dtype=int8)
+
+
+
+
+Binary operators
+----------------
+
+``ulab`` implements the ``+``, ``-``, ``*``, ``/``, ``**``, ``<``,
+``>``, ``<=``, ``>=``, ``==``, ``!=``, ``+=``, ``-=``, ``*=``, ``/=``,
+``**=`` binary operators that work element-wise. Broadcasting is
+available, meaning that the two operands do not even have to have the
+same shape. If the lengths along the respective axes are equal, or one
+of them is 1, or the axis is missing, the element-wise operation can
+still be carried out. A thorough explanation of broadcasting can be
+found under https://numpy.org/doc/stable/user/basics.broadcasting.html.
+
+**WARNING**: note that relational operators (``<``, ``>``, ``<=``,
+``>=``, ``==``, ``!=``) should have the ``ndarray`` on their left hand
+side, when compared to scalars. This means that the following works
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3])
+ print(a > 2)
+
+.. parsed-literal::
+
+ array([False, False, True], dtype=bool)
+
+
+
+
+while the equivalent statement, ``2 < a``, will raise a ``TypeError``
+exception:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3])
+ print(2 < a)
+
+.. parsed-literal::
+
+
+ Traceback (most recent call last):
+ File "/dev/shm/micropython.py", line 5, in <module>
+ TypeError: unsupported types for __lt__: 'int', 'ndarray'
+
+
+
+**WARNING:** ``circuitpython`` users should use the ``equal``, and
+``not_equal`` operators instead of ``==``, and ``!=``. See the section
+on `array comparison <#Comparison-of-arrays>`__ for details.
+
+Upcasting
+~~~~~~~~~
+
+Binary operations require special attention, because two arrays with
+different typecodes can be the operands of an operation, in which case
+it is not trivial, what the typecode of the result is. This decision on
+the result’s typecode is called upcasting. Since the number of typecodes
+in ``ulab`` is significantly smaller than in ``numpy``, we have to
+define new upcasting rules. Where possible, I followed ``numpy``\ ’s
+conventions.
+
+``ulab`` observes the following upcasting rules:
+
+1. Operations on two ``ndarray``\ s of the same ``dtype`` preserve their
+ ``dtype``, even when the results overflow.
+
+2. if either of the operands is a float, the result is automatically a
+ float
+
+3. When one of the operands is a scalar, it will internally be turned
+ into a single-element ``ndarray`` with the *smallest* possible
+ ``dtype``. Thus, e.g., if the scalar is 123, it will be converted
+ into an array of ``dtype`` ``uint8``, while -1000 will be converted
+ into ``int16``. An ``mp_obj_float``, will always be promoted to
+ ``dtype`` ``float``. Other micropython types (e.g., lists, tuples,
+ etc.) raise a ``TypeError`` exception.
+
+4.
+
+============== =============== =========== ============
+left hand side right hand side ulab result numpy result
+============== =============== =========== ============
+``uint8`` ``int8`` ``int16`` ``int16``
+``uint8`` ``int16`` ``int16`` ``int16``
+``uint8`` ``uint16`` ``uint16`` ``uint16``
+``int8`` ``int16`` ``int16`` ``int16``
+``int8`` ``uint16`` ``uint16`` ``int32``
+``uint16`` ``int16`` ``float`` ``int32``
+============== =============== =========== ============
+
+Note that the last two operations are promoted to ``int32`` in
+``numpy``.
+
+**WARNING:** Due to the lower number of available data types, the
+upcasting rules of ``ulab`` are slightly different to those of
+``numpy``. Watch out for this, when porting code!
+
+Upcasting can be seen in action in the following snippet:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4], dtype=np.uint8)
+ b = np.array([1, 2, 3, 4], dtype=np.int8)
+ print("a:\t", a)
+ print("b:\t", b)
+ print("a+b:\t", a+b)
+
+ c = np.array([1, 2, 3, 4], dtype=np.float)
+ print("\na:\t", a)
+ print("c:\t", c)
+ print("a*c:\t", a*c)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4], dtype=uint8)
+ b: array([1, 2, 3, 4], dtype=int8)
+ a+b: array([2, 4, 6, 8], dtype=int16)
+
+ a: array([1, 2, 3, 4], dtype=uint8)
+ c: array([1.0, 2.0, 3.0, 4.0], dtype=float64)
+ a*c: array([1.0, 4.0, 9.0, 16.0], dtype=float64)
+
+
+
+
+Benchmarks
+~~~~~~~~~~
+
+The following snippet compares the performance of binary operations to a
+possible implementation in python. For the time measurement, we will
+take the following snippet from the micropython manual:
+
+.. code::
+
+ # code to be run in micropython
+
+ import utime
+
+ def timeit(f, *args, **kwargs):
+ func_name = str(f).split(' ')[1]
+ def new_func(*args, **kwargs):
+ t = utime.ticks_us()
+ result = f(*args, **kwargs)
+ print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')
+ return result
+ return new_func
+
+.. parsed-literal::
+
+
+
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ @timeit
+ def py_add(a, b):
+ return [a[i]+b[i] for i in range(1000)]
+
+ @timeit
+ def py_multiply(a, b):
+ return [a[i]*b[i] for i in range(1000)]
+
+ @timeit
+ def ulab_add(a, b):
+ return a + b
+
+ @timeit
+ def ulab_multiply(a, b):
+ return a * b
+
+ a = [0.0]*1000
+ b = range(1000)
+
+ print('python add:')
+ py_add(a, b)
+
+ print('\npython multiply:')
+ py_multiply(a, b)
+
+ a = np.linspace(0, 10, num=1000)
+ b = np.ones(1000)
+
+ print('\nulab add:')
+ ulab_add(a, b)
+
+ print('\nulab multiply:')
+ ulab_multiply(a, b)
+
+.. parsed-literal::
+
+ python add:
+ execution time: 10051 us
+
+ python multiply:
+ execution time: 14175 us
+
+ ulab add:
+ execution time: 222 us
+
+ ulab multiply:
+ execution time: 213 us
+
+
+
+The python implementation above is not perfect, and certainly, there is
+much room for improvement. However, the factor of 50 difference in
+execution time is very spectacular. This is nothing but a consequence of
+the fact that the ``ulab`` functions run ``C`` code, with very little
+python overhead. The factor of 50 appears to be quite universal: the FFT
+routine obeys similar scaling (see `Speed of FFTs <#Speed-of-FFTs>`__),
+and this number came up with font rendering, too: `fast font rendering
+on graphical
+displays <https://forum.micropython.org/viewtopic.php?f=15&t=5815&p=33362&hilit=ufont#p33383>`__.
+
+Comparison operators
+--------------------
+
+The smaller than, greater than, smaller or equal, and greater or equal
+operators return a vector of Booleans indicating the positions
+(``True``), where the condition is satisfied.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint8)
+ print(a < 5)
+
+.. parsed-literal::
+
+ array([True, True, True, True, False, False, False, False], dtype=bool)
+
+
+
+
+**WARNING**: at the moment, due to ``micropython``\ ’s implementation
+details, the ``ndarray`` must be on the left hand side of the relational
+operators.
+
+That is, while ``a < 5`` and ``5 > a`` have the same meaning, the
+following code will not work:
+
+.. code::
+
+ # code to be run in micropython
+
+ import ulab as np
+
+ a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint8)
+ print(5 > a)
+
+.. parsed-literal::
+
+
+ Traceback (most recent call last):
+ File "/dev/shm/micropython.py", line 5, in <module>
+ TypeError: unsupported types for __gt__: 'int', 'ndarray'
+
+
+
+Iterating over arrays
+---------------------
+
+``ndarray``\ s are iterable, which means that their elements can also be
+accessed as can the elements of a list, tuple, etc. If the array is
+one-dimensional, the iterator returns scalars, otherwise a new
+reduced-dimensional *view* is created and returned.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)
+ b = np.array([range(5), range(10, 15, 1), range(20, 25, 1), range(30, 35, 1)], dtype=np.uint8)
+
+ print("a:\t", a)
+
+ for i, _a in enumerate(a):
+ print("element %d in a:"%i, _a)
+
+ print("\nb:\t", b)
+
+ for i, _b in enumerate(b):
+ print("element %d in b:"%i, _b)
+
+.. parsed-literal::
+
+ a: array([1, 2, 3, 4, 5], dtype=uint8)
+ element 0 in a: 1
+ element 1 in a: 2
+ element 2 in a: 3
+ element 3 in a: 4
+ element 4 in a: 5
+
+ b: array([[0, 1, 2, 3, 4],
+ [10, 11, 12, 13, 14],
+ [20, 21, 22, 23, 24],
+ [30, 31, 32, 33, 34]], dtype=uint8)
+ element 0 in b: array([0, 1, 2, 3, 4], dtype=uint8)
+ element 1 in b: array([10, 11, 12, 13, 14], dtype=uint8)
+ element 2 in b: array([20, 21, 22, 23, 24], dtype=uint8)
+ element 3 in b: array([30, 31, 32, 33, 34], dtype=uint8)
+
+
+
+
+Slicing and indexing
+--------------------
+
+Views vs. copies
+~~~~~~~~~~~~~~~~
+
+``numpy`` has a very important concept called *views*, which is a
+powerful extension of ``python``\ ’s own notion of slicing. Slices are
+special python objects of the form
+
+.. code:: python
+
+ slice = start:end:stop
+
+where ``start``, ``end``, and ``stop`` are (not necessarily
+non-negative) integers. Not all of these three numbers must be specified
+in an index, in fact, all three of them can be missing. The interpreter
+takes care of filling in the missing values. (Note that slices cannot be
+defined in this way, only there, where an index is expected.) For a good
+explanation on how slices work in python, you can read the stackoverflow
+question
+https://stackoverflow.com/questions/509211/understanding-slice-notation.
+
+In order to see what slicing does, let us take the string
+``a = '012345679'``! We can extract every second character by creating
+the slice ``::2``, which is equivalent to ``0:len(a):2``, i.e.,
+increments the character pointer by 2 starting from 0, and traversing
+the string up to the very end.
+
+.. code::
+
+ # code to be run in CPython
+
+ string = '0123456789'
+ string[::2]
+
+
+
+.. parsed-literal::
+
+ '02468'
+
+
+
+Now, we can do the same with numerical arrays.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(10), dtype=np.uint8)
+ print('a:\t', a)
+
+ print('a[::2]:\t', a[::2])
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)
+ a[::2]: array([0, 2, 4, 6, 8], dtype=uint8)
+
+
+
+
+This looks similar to ``string`` above, but there is a very important
+difference that is not so obvious. Namely, ``string[::2]`` produces a
+partial copy of ``string``, while ``a[::2]`` only produces a *view* of
+``a``. What this means is that ``a``, and ``a[::2]`` share their data,
+and the only difference between the two is, how the data are read out.
+In other words, internally, ``a[::2]`` has the same data pointer as
+``a``. We can easily convince ourselves that this is indeed the case by
+calling the `ndinfo <#The_ndinfo_function>`__ function: the *data
+pointer* entry is the same in the two printouts.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(10), dtype=np.uint8)
+ print('a: ', a, '\n')
+ np.ndinfo(a)
+ print('\n' + '='*20)
+ print('a[::2]: ', a[::2], '\n')
+ np.ndinfo(a[::2])
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)
+
+ class: ndarray
+ shape: (10,)
+ strides: (1,)
+ itemsize: 1
+ data pointer: 0x7ff6c6193220
+ type: uint8
+
+ ====================
+ a[::2]: array([0, 2, 4, 6, 8], dtype=uint8)
+
+ class: ndarray
+ shape: (5,)
+ strides: (2,)
+ itemsize: 1
+ data pointer: 0x7ff6c6193220
+ type: uint8
+
+
+
+
+If you are still a bit confused about the meaning of *views*, the
+section `Slicing and assigning to
+slices <#Slicing-and-assigning-to-slices>`__ should clarify the issue.
+
+Indexing
+~~~~~~~~
+
+The simplest form of indexing is specifying a single integer between the
+square brackets as in
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(10), dtype=np.uint8)
+ print("a: ", a)
+ print("the first, and last element of a:\n", a[0], a[-1])
+ print("the second, and last but one element of a:\n", a[1], a[-2])
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)
+ the first, and last element of a:
+ 0 9
+ the second, and last but one element of a:
+ 1 8
+
+
+
+
+Indexing can be applied to higher-dimensional tensors, too. When the
+length of the indexing sequences is smaller than the number of
+dimensions, a new *view* is returned, otherwise, we get a single number.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.uint8).reshape((3, 3))
+ print("a:\n", a)
+ print("a[0]:\n", a[0])
+ print("a[1,1]: ", a[1,1])
+
+.. parsed-literal::
+
+ a:
+ array([[0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8]], dtype=uint8)
+ a[0]:
+ array([[0, 1, 2]], dtype=uint8)
+ a[1,1]: 4
+
+
+
+
+Indices can also be a list of Booleans. By using a Boolean list, we can
+select those elements of an array that satisfy a specific condition. At
+the moment, such indexing is defined for row vectors only; when the rank
+of the tensor is higher than 1, the function raises a
+``NotImplementedError`` exception, though this will be rectified in a
+future version of ``ulab``.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.float)
+ print("a:\t", a)
+ print("a < 5:\t", a[a < 5])
+
+.. parsed-literal::
+
+ a: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)
+ a < 5: array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)
+
+
+
+
+Indexing with Boolean arrays can take more complicated expressions. This
+is a very concise way of comparing two vectors, e.g.:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.uint8)
+ b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)
+ print("a:\t", a)
+ print("\na**2:\t", a*a)
+ print("\nb:\t", b)
+ print("\n100*sin(b):\t", np.sin(b)*100.0)
+ print("\na[a*a > np.sin(b)*100.0]:\t", a[a*a > np.sin(b)*100.0])
+
+.. parsed-literal::
+
+ a: array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)
+
+ a**2: array([0, 1, 4, 9, 16, 25, 36, 49, 64], dtype=uint16)
+
+ b: array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=uint8)
+
+ 100*sin(b): array([-75.68024953079282, -75.68024953079282, -75.68024953079282, 14.11200080598672, 14.11200080598672, 14.11200080598672, 42.01670368266409, 42.01670368266409, 42.01670368266409], dtype=float)
+
+ a[a*a > np.sin(b)*100.0]: array([0, 1, 2, 4, 5, 7, 8], dtype=uint8)
+
+
+
+
+Boolean indices can also be used in assignments, if the array is
+one-dimensional. The following example replaces the data in an array,
+wherever some condition is fulfilled.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.uint8)
+ b = np.array(range(9)) + 12
+
+ print(a[b < 15])
+
+ a[b < 15] = 123
+ print(a)
+
+.. parsed-literal::
+
+ array([0, 1, 2], dtype=uint8)
+ array([123, 123, 123, 3, 4, 5, 6, 7, 8], dtype=uint8)
+
+
+
+
+On the right hand side of the assignment we can even have another array.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array(range(9), dtype=np.uint8)
+ b = np.array(range(9)) + 12
+
+ print(a[b < 15], b[b < 15])
+
+ a[b < 15] = b[b < 15]
+ print(a)
+
+.. parsed-literal::
+
+ array([0, 1, 2], dtype=uint8) array([12.0, 13.0, 14.0], dtype=float)
+ array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)
+
+
+
+
+Slicing and assigning to slices
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also generate sub-arrays by specifying slices as the index of an
+array. Slices are special python objects of the form
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)
+ print('a:\n', a)
+
+ # the first row
+ print('\na[0]:\n', a[0])
+
+ # the first two elements of the first row
+ print('\na[0,:2]:\n', a[0,:2])
+
+ # the zeroth element in each row (also known as the zeroth column)
+ print('\na[:,0]:\n', a[:,0])
+
+ # the last row
+ print('\na[-1]:\n', a[-1])
+
+ # the last two rows backwards
+ print('\na[-1:-3:-1]:\n', a[-1:-3:-1])
+
+.. parsed-literal::
+
+ a:
+ array([[1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9]], dtype=uint8)
+
+ a[0]:
+ array([[1, 2, 3]], dtype=uint8)
+
+ a[0,:2]:
+ array([[1, 2]], dtype=uint8)
+
+ a[:,0]:
+ array([[1],
+ [4],
+ [7]], dtype=uint8)
+
+ a[-1]:
+ array([[7, 8, 9]], dtype=uint8)
+
+ a[-1:-3:-1]:
+ array([[7, 8, 9],
+ [4, 5, 6]], dtype=uint8)
+
+
+
+
+Assignment to slices can be done for the whole slice, per row, and per
+column. A couple of examples should make these statements clearer:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.zeros((3, 3), dtype=np.uint8)
+ print('a:\n', a)
+
+ # assigning to the whole row
+ a[0] = 1
+ print('\na[0] = 1\n', a)
+
+ a = np.zeros((3, 3), dtype=np.uint8)
+
+ # assigning to a column
+ a[:,2] = 3.0
+ print('\na[:,0]:\n', a)
+
+.. parsed-literal::
+
+ a:
+ array([[0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]], dtype=uint8)
+
+ a[0] = 1
+ array([[1, 1, 1],
+ [0, 0, 0],
+ [0, 0, 0]], dtype=uint8)
+
+ a[:,0]:
+ array([[0, 0, 3],
+ [0, 0, 3],
+ [0, 0, 3]], dtype=uint8)
+
+
+
+
+Now, you should notice that we re-set the array ``a`` after the first
+assignment. Do you care to see what happens, if we do not do that? Well,
+here are the results:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.zeros((3, 3), dtype=np.uint8)
+ b = a[:,:]
+ # assign 1 to the first row
+ b[0] = 1
+
+ # assigning to the last column
+ b[:,2] = 3
+ print('a: ', a)
+
+.. parsed-literal::
+
+ a: array([[1, 1, 3],
+ [0, 0, 3],
+ [0, 0, 3]], dtype=uint8)
+
+
+
+
+Note that both assignments involved ``b``, and not ``a``, yet, when we
+print out ``a``, its entries are updated. This proves our earlier
+statement about the behaviour of *views*: in the statement
+``b = a[:,:]`` we simply created a *view* of ``a``, and not a *deep*
+copy of it, meaning that whenever we modify ``b``, we actually modify
+``a``, because the underlying data container of ``a`` and ``b`` are
+shared between the two object. Having a single data container for two
+seemingly different objects provides an extremely powerful way of
+manipulating sub-sets of numerical data.
+
+If you want to work on a *copy* of your data, you can use the ``.copy``
+method of the ``ndarray``. The following snippet should drive the point
+home:
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.zeros((3, 3), dtype=np.uint8)
+ b = a.copy()
+
+ # get the address of the underlying data pointer
+
+ np.ndinfo(a)
+ print()
+ np.ndinfo(b)
+
+ # assign 1 to the first row of b, and do not touch a
+ b[0] = 1
+
+ print()
+ print('a: ', a)
+ print('='*20)
+ print('b: ', b)
+
+.. parsed-literal::
+
+ class: ndarray
+ shape: (3, 3)
+ strides: (3, 1)
+ itemsize: 1
+ data pointer: 0x7ff737ea3220
+ type: uint8
+
+ class: ndarray
+ shape: (3, 3)
+ strides: (3, 1)
+ itemsize: 1
+ data pointer: 0x7ff737ea3340
+ type: uint8
+
+ a: array([[0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]], dtype=uint8)
+ ====================
+ b: array([[1, 1, 1],
+ [0, 0, 0],
+ [0, 0, 0]], dtype=uint8)
+
+
+
+
+The ``.copy`` method can also be applied to views: below, ``a[0]`` is a
+*view* of ``a``, out of which we create a *deep copy* called ``b``. This
+is a row vector now. We can then do whatever we want to with ``b``, and
+that leaves ``a`` unchanged.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+
+ a = np.zeros((3, 3), dtype=np.uint8)
+ b = a[0].copy()
+ print('b: ', b)
+ print('='*20)
+ # assign 1 to the first entry of b, and do not touch a
+ b[0] = 1
+ print('a: ', a)
+ print('='*20)
+ print('b: ', b)
+
+.. parsed-literal::
+
+ b: array([0, 0, 0], dtype=uint8)
+ ====================
+ a: array([[0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]], dtype=uint8)
+ ====================
+ b: array([1, 0, 0], dtype=uint8)
+
+
+
+
+The fact that the underlying data of a view is the same as that of the
+original array has another important consequence, namely, that the
+creation of a view is cheap. Both in terms of RAM, and execution time. A
+view is really nothing but a short header with a data array that already
+exists, and is filled up. Hence, creating the view requires only the
+creation of its header. This operation is fast, and uses virtually no
+RAM.
diff --git a/circuitpython/extmod/ulab/docs/manual/source/ulab-programming.rst b/circuitpython/extmod/ulab/docs/manual/source/ulab-programming.rst
new file mode 100644
index 0000000..ff1788b
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/ulab-programming.rst
@@ -0,0 +1,911 @@
+
+Programming ulab
+================
+
+Earlier we have seen, how ``ulab``\ ’s functions and methods can be
+accessed in ``micropython``. This last section of the book explains, how
+these functions are implemented. By the end of this chapter, not only
+would you be able to extend ``ulab``, and write your own
+``numpy``-compatible functions, but through a deeper understanding of
+the inner workings of the functions, you would also be able to see what
+the trade-offs are at the ``python`` level.
+
+Code organisation
+-----------------
+
+As mentioned earlier, the ``python`` functions are organised into
+sub-modules at the C level. The C sub-modules can be found in
+``./ulab/code/``.
+
+The ``ndarray`` object
+----------------------
+
+General comments
+~~~~~~~~~~~~~~~~
+
+``ndarrays`` are efficient containers of numerical data of the same type
+(i.e., signed/unsigned chars, signed/unsigned integers or
+``mp_float_t``\ s, which, depending on the platform, are either C
+``float``\ s, or C ``double``\ s). Beyond storing the actual data in the
+void pointer ``*array``, the type definition has eight additional
+members (on top of the ``base`` type). Namely, the ``dtype``, which
+tells us, how the bytes are to be interpreted. Moreover, the
+``itemsize``, which stores the size of a single entry in the array,
+``boolean``, an unsigned integer, which determines, whether the arrays
+is to be treated as a set of Booleans, or as numerical data, ``ndim``,
+the number of dimensions (``uint8_t``), ``len``, the length of the array
+(the number of entries), the shape (``*size_t``), the strides
+(``*int32_t``). The length is simply the product of the numbers in
+``shape``.
+
+The type definition is as follows:
+
+.. code:: c
+
+ typedef struct _ndarray_obj_t {
+ mp_obj_base_t base;
+ uint8_t dtype;
+ uint8_t itemsize;
+ uint8_t boolean;
+ uint8_t ndim;
+ size_t len;
+ size_t shape[ULAB_MAX_DIMS];
+ int32_t strides[ULAB_MAX_DIMS];
+ void *array;
+ } ndarray_obj_t;
+
+Memory layout
+~~~~~~~~~~~~~
+
+The values of an ``ndarray`` are stored in a contiguous segment in the
+RAM. The ``ndarray`` can be dense, meaning that all numbers in the
+linear memory segment belong to a linar combination of coordinates, and
+it can also be sparse, i.e., some elements of the linear storage space
+will be skipped, when the elements of the tensor are traversed.
+
+In the RAM, the position of the item
+:math:`M(n_1, n_2, ..., n_{k-1}, n_k)` in a dense tensor of rank
+:math:`k` is given by the linear combination
+
+:raw-latex:`\begin{equation}
+P(n_1, n_2, ..., n_{k-1}, n_k) = n_1 s_1 + n_2 s_2 + ... + n_{k-1}s_{k-1} + n_ks_k = \sum_{i=1}^{k}n_is_i
+\end{equation}` where :math:`s_i` are the strides of the tensor, defined
+as
+
+:raw-latex:`\begin{equation}
+s_i = \prod_{j=i+1}^k l_j
+\end{equation}`
+
+where :math:`l_j` is length of the tensor along the :math:`j`\ th axis.
+When the tensor is sparse (e.g., when the tensor is sliced), the strides
+along a particular axis will be multiplied by a non-zero integer. If
+this integer is different to :math:`\pm 1`, the linear combination above
+cannot access all elements in the RAM, i.e., some numbers will be
+skipped. Note that :math:`|s_1| > |s_2| > ... > |s_{k-1}| > |s_k|`, even
+if the tensor is sparse. The statement is trivial for dense tensors, and
+it follows from the definition of :math:`s_i`. For sparse tensors, a
+slice cannot have a step larger than the shape along that axis. But for
+dense tensors, :math:`s_i/s_{i+1} = l_i`.
+
+When creating a *view*, we simply re-calculate the ``strides``, and
+re-set the ``*array`` pointer.
+
+Iterating over elements of a tensor
+-----------------------------------
+
+The ``shape`` and ``strides`` members of the array tell us how we have
+to move our pointer, when we want to read out the numbers. For technical
+reasons that will become clear later, the numbers in ``shape`` and in
+``strides`` are aligned to the right, and begin on the right hand side,
+i.e., if the number of possible dimensions is ``ULAB_MAX_DIMS``, then
+``shape[ULAB_MAX_DIMS-1]`` is the length of the last axis,
+``shape[ULAB_MAX_DIMS-2]`` is the length of the last but one axis, and
+so on. If the number of actual dimensions, ``ndim < ULAB_MAX_DIMS``, the
+first ``ULAB_MAX_DIMS - ndim`` entries in ``shape`` and ``strides`` will
+be equal to zero, but they could, in fact, be assigned any value,
+because these will never be accessed in an operation.
+
+With this definition of the strides, the linear combination in
+:math:`P(n_1, n_2, ..., n_{k-1}, n_k)` is a one-to-one mapping from the
+space of tensor coordinates, :math:`(n_1, n_2, ..., n_{k-1}, n_k)`, and
+the coordinate in the linear array,
+:math:`n_1s_1 + n_2s_2 + ... + n_{k-1}s_{k-1} + n_ks_k`, i.e., no two
+distinct sets of coordinates will result in the same position in the
+linear array.
+
+Since the ``strides`` are given in terms of bytes, when we iterate over
+an array, the void data pointer is usually cast to ``uint8_t``, and the
+values are converted using the proper data type stored in
+``ndarray->dtype``. However, there might be cases, when it makes perfect
+sense to cast ``*array`` to a different type, in which case the
+``strides`` have to be re-scaled by the value of ``ndarray->itemsize``.
+
+Iterating using the unwrapped loops
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The following macro definition is taken from
+`vector.h <https://github.com/v923z/micropython-ulab/blob/master/code/numpy/vector/vector.h>`__,
+and demonstrates, how we can iterate over a single array in four
+dimensions.
+
+.. code:: c
+
+ #define ITERATE_VECTOR(type, array, source, sarray) do {
+ size_t i=0;
+ do {
+ size_t j = 0;
+ do {
+ size_t k = 0;
+ do {
+ size_t l = 0;
+ do {
+ *(array)++ = f(*((type *)(sarray)));
+ (sarray) += (source)->strides[ULAB_MAX_DIMS - 1];
+ l++;
+ } while(l < (source)->shape[ULAB_MAX_DIMS-1]);
+ (sarray) -= (source)->strides[ULAB_MAX_DIMS - 1] * (source)->shape[ULAB_MAX_DIMS-1];
+ (sarray) += (source)->strides[ULAB_MAX_DIMS - 2];
+ k++;
+ } while(k < (source)->shape[ULAB_MAX_DIMS-2]);
+ (sarray) -= (source)->strides[ULAB_MAX_DIMS - 2] * (source)->shape[ULAB_MAX_DIMS-2];
+ (sarray) += (source)->strides[ULAB_MAX_DIMS - 3];
+ j++;
+ } while(j < (source)->shape[ULAB_MAX_DIMS-3]);
+ (sarray) -= (source)->strides[ULAB_MAX_DIMS - 3] * (source)->shape[ULAB_MAX_DIMS-3];
+ (sarray) += (source)->strides[ULAB_MAX_DIMS - 4];
+ i++;
+ } while(i < (source)->shape[ULAB_MAX_DIMS-4]);
+ } while(0)
+
+We start with the innermost loop, the one recursing ``l``. ``array`` is
+already of type ``mp_float_t``, while the source array, ``sarray``, has
+been cast to ``uint8_t`` in the calling function. The numbers contained
+in ``sarray`` have to be read out in the proper type dictated by
+``ndarray->dtype``. This is what happens in the statement
+``*((type *)(sarray))``, and this number is then fed into the function
+``f``. Vectorised mathematical functions produce *dense* arrays, and for
+this reason, we can simply advance the ``array`` pointer.
+
+The advancing of the ``sarray`` pointer is a bit more involving: first,
+in the innermost loop, we simply move forward by the amount given by the
+last stride, which is ``(source)->strides[ULAB_MAX_DIMS - 1]``, because
+the ``shape`` and the ``strides`` are aligned to the right. We move the
+pointer as many times as given by ``(source)->shape[ULAB_MAX_DIMS-1]``,
+which is the length of the very last axis. Hence the the structure of
+the loop
+
+.. code:: c
+
+ size_t l = 0;
+ do {
+ ...
+ l++;
+ } while(l < (source)->shape[ULAB_MAX_DIMS-1]);
+
+Once we have exhausted the last axis, we have to re-wind the pointer,
+and advance it by an amount given by the last but one stride. Keep in
+mind that in the the innermost loop we moved our pointer
+``(source)->shape[ULAB_MAX_DIMS-1]`` times by
+``(source)->strides[ULAB_MAX_DIMS - 1]``, i.e., we re-wind it by moving
+it backwards by
+``(source)->strides[ULAB_MAX_DIMS - 1] * (source)->shape[ULAB_MAX_DIMS-1]``.
+In the next step, we move forward by
+``(source)->strides[ULAB_MAX_DIMS - 2]``, which is the last but one
+stride.
+
+.. code:: c
+
+ (sarray) -= (source)->strides[ULAB_MAX_DIMS - 1] * (source)->shape[ULAB_MAX_DIMS-1];
+ (sarray) += (source)->strides[ULAB_MAX_DIMS - 2];
+
+This pattern must be repeated for each axis of the array, and this is
+how we arrive at the four nested loops listed above.
+
+Re-winding arrays by means of a function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In addition to un-wrapping the iteration loops by means of macros, there
+is another way of traversing all elements of a tensor: we note that,
+since :math:`|s_1| > |s_2| > ... > |s_{k-1}| > |s_k|`,
+:math:`P(n1, n2, ..., n_{k-1}, n_k)` changes most slowly in the last
+coordinate. Hence, if we start from the very beginning, (:math:`n_i = 0`
+for all :math:`i`), and walk along the linear RAM segment, we increment
+the value of :math:`n_k` as long as :math:`n_k < l_k`. Once
+:math:`n_k = l_k`, we have to reset :math:`n_k` to 0, and increment
+:math:`n_{k-1}` by one. After each such round, :math:`n_{k-1}` will be
+incremented by one, as long as :math:`n_{k-1} < l_{k-1}`. Once
+:math:`n_{k-1} = l_{k-1}`, we reset both :math:`n_k`, and
+:math:`n_{k-1}` to 0, and increment :math:`n_{k-2}` by one.
+
+Rewinding the arrays in this way is implemented in the function
+``ndarray_rewind_array`` in
+`ndarray.c <https://github.com/v923z/micropython-ulab/blob/master/code/ndarray.c>`__.
+
+.. code:: c
+
+ void ndarray_rewind_array(uint8_t ndim, uint8_t *array, size_t *shape, int32_t *strides, size_t *coords) {
+ // resets the data pointer of a single array, whenever an axis is full
+ // since we always iterate over the very last axis, we have to keep track of
+ // the last ndim-2 axes only
+ array -= shape[ULAB_MAX_DIMS - 1] * strides[ULAB_MAX_DIMS - 1];
+ array += strides[ULAB_MAX_DIMS - 2];
+ for(uint8_t i=1; i < ndim-1; i++) {
+ coords[ULAB_MAX_DIMS - 1 - i] += 1;
+ if(coords[ULAB_MAX_DIMS - 1 - i] == shape[ULAB_MAX_DIMS - 1 - i]) { // we are at a dimension boundary
+ array -= shape[ULAB_MAX_DIMS - 1 - i] * strides[ULAB_MAX_DIMS - 1 - i];
+ array += strides[ULAB_MAX_DIMS - 2 - i];
+ coords[ULAB_MAX_DIMS - 1 - i] = 0;
+ coords[ULAB_MAX_DIMS - 2 - i] += 1;
+ } else { // coordinates can change only, if the last coordinate changes
+ return;
+ }
+ }
+ }
+
+and the function would be called as in the snippet below. Note that the
+innermost loop is factored out, so that we can save the ``if(...)``
+statement for the last axis.
+
+.. code:: c
+
+ size_t *coords = ndarray_new_coords(results->ndim);
+ for(size_t i=0; i < results->len/results->shape[ULAB_MAX_DIMS -1]; i++) {
+ size_t l = 0;
+ do {
+ ...
+ l++;
+ } while(l < results->shape[ULAB_MAX_DIMS - 1]);
+ ndarray_rewind_array(results->ndim, array, results->shape, strides, coords);
+ } while(0)
+
+The advantage of this method is that the implementation is independent
+of the number of dimensions: the iteration requires more or less the
+same flash space for 2 dimensions as for 22. However, the price we have
+to pay for this convenience is the extra function call.
+
+Iterating over two ndarrays simultaneously: broadcasting
+--------------------------------------------------------
+
+Whenever we invoke a binary operator, call a function with two arguments
+of ``ndarray`` type, or assign something to an ``ndarray``, we have to
+iterate over two views at the same time. The task is trivial, if the two
+``ndarray``\ s in question have the same shape (but not necessarily the
+same set of strides), because in this case, we can still iterate in the
+same loop. All that happens is that we move two data pointers in sync.
+
+The problem becomes a bit more involving, when the shapes of the two
+``ndarray``\ s are not identical. For such cases, ``numpy`` defines
+so-called broadcasting, which boils down to two rules.
+
+1. The shapes in the tensor with lower rank has to be prepended with
+ axes of size 1 till the two ranks become equal.
+2. Along all axes the two tensors should have the same size, or one of
+ the sizes must be 1.
+
+If, after applying the first rule the second is not satisfied, the two
+``ndarray``\ s cannot be broadcast together.
+
+Now, let us suppose that we have two compatible ``ndarray``\ s, i.e.,
+after applying the first rule, the second is satisfied. How do we
+iterate over the elements in the tensors?
+
+We should recall, what exactly we do, when iterating over a single
+array: normally, we move the data pointer by the last stride, except,
+when we arrive at a dimension boundary (when the last axis is
+exhausted). At that point, we move the pointer by an amount dictated by
+the strides. And this is the key: *dictated by the strides*. Now, if we
+have two arrays that are originally not compatible, we define new
+strides for them, and use these in the iteration. With that, we are back
+to the case, where we had two compatible arrays.
+
+Now, let us look at the second broadcasting rule: if the two arrays have
+the same size, we take both ``ndarray``\ s’ strides along that axis. If,
+on the other hand, one of the ``ndarray``\ s is of length 1 along one of
+its axes, we set the corresponding strides to 0. This will ensure that
+that data pointer is not moved, when we iterate over both ``ndarray``\ s
+at the same time.
+
+Thus, in order to implement broadcasting, we first have to check,
+whether the two above-mentioned rules can be satisfied, and if so, we
+have to find the two new sets strides.
+
+The ``ndarray_can_broadcast`` function from
+`ndarray.c <https://github.com/v923z/micropython-ulab/blob/master/code/ndarray.c>`__
+takes two ``ndarray``\ s, and returns ``true``, if the two arrays can be
+broadcast together. At the same time, it also calculates new strides for
+the two arrays, so that they can be iterated over at the same time.
+
+.. code:: c
+
+ bool ndarray_can_broadcast(ndarray_obj_t *lhs, ndarray_obj_t *rhs, uint8_t *ndim, size_t *shape, int32_t *lstrides, int32_t *rstrides) {
+ // returns True or False, depending on, whether the two arrays can be broadcast together
+ // numpy's broadcasting rules are as follows:
+ //
+ // 1. the two shapes are either equal
+ // 2. one of the shapes is 1
+ memset(lstrides, 0, sizeof(size_t)*ULAB_MAX_DIMS);
+ memset(rstrides, 0, sizeof(size_t)*ULAB_MAX_DIMS);
+ lstrides[ULAB_MAX_DIMS - 1] = lhs->strides[ULAB_MAX_DIMS - 1];
+ rstrides[ULAB_MAX_DIMS - 1] = rhs->strides[ULAB_MAX_DIMS - 1];
+ for(uint8_t i=ULAB_MAX_DIMS; i > 0; i--) {
+ if((lhs->shape[i-1] == rhs->shape[i-1]) || (lhs->shape[i-1] == 0) || (lhs->shape[i-1] == 1) ||
+ (rhs->shape[i-1] == 0) || (rhs->shape[i-1] == 1)) {
+ shape[i-1] = MAX(lhs->shape[i-1], rhs->shape[i-1]);
+ if(shape[i-1] > 0) (*ndim)++;
+ if(lhs->shape[i-1] < 2) {
+ lstrides[i-1] = 0;
+ } else {
+ lstrides[i-1] = lhs->strides[i-1];
+ }
+ if(rhs->shape[i-1] < 2) {
+ rstrides[i-1] = 0;
+ } else {
+ rstrides[i-1] = rhs->strides[i-1];
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+A good example of how the function would be called can be found in
+`vector.c <https://github.com/v923z/micropython-ulab/blob/master/code/numpy/vector/vector.c>`__,
+in the ``vector_arctan2`` function:
+
+.. code:: c
+
+ mp_obj_t vectorise_arctan2(mp_obj_t y, mp_obj_t x) {
+ ...
+ uint8_t ndim = 0;
+ size_t *shape = m_new(size_t, ULAB_MAX_DIMS);
+ int32_t *xstrides = m_new(int32_t, ULAB_MAX_DIMS);
+ int32_t *ystrides = m_new(int32_t, ULAB_MAX_DIMS);
+ if(!ndarray_can_broadcast(ndarray_x, ndarray_y, &ndim, shape, xstrides, ystrides)) {
+ mp_raise_ValueError(translate("operands could not be broadcast together"));
+ m_del(size_t, shape, ULAB_MAX_DIMS);
+ m_del(int32_t, xstrides, ULAB_MAX_DIMS);
+ m_del(int32_t, ystrides, ULAB_MAX_DIMS);
+ }
+
+ uint8_t *xarray = (uint8_t *)ndarray_x->array;
+ uint8_t *yarray = (uint8_t *)ndarray_y->array;
+
+ ndarray_obj_t *results = ndarray_new_dense_ndarray(ndim, shape, NDARRAY_FLOAT);
+ mp_float_t *rarray = (mp_float_t *)results->array;
+ ...
+
+After the new strides have been calculated, the iteration loop is
+identical to what we discussed in the previous section.
+
+Contracting an ``ndarray``
+--------------------------
+
+There are many operations that reduce the number of dimensions of an
+``ndarray`` by 1, i.e., that remove an axis from the tensor. The drill
+is the same as before, with the exception that first we have to remove
+the ``strides`` and ``shape`` that corresponds to the axis along which
+we intend to contract. The ``numerical_reduce_axes`` function from
+`numerical.c <https://github.com/v923z/micropython-ulab/blob/master/code/numerical/numerical.c>`__
+does that.
+
+.. code:: c
+
+ static void numerical_reduce_axes(ndarray_obj_t *ndarray, int8_t axis, size_t *shape, int32_t *strides) {
+ // removes the values corresponding to a single axis from the shape and strides array
+ uint8_t index = ULAB_MAX_DIMS - ndarray->ndim + axis;
+ if((ndarray->ndim == 1) && (axis == 0)) {
+ index = 0;
+ shape[ULAB_MAX_DIMS - 1] = 0;
+ return;
+ }
+ for(uint8_t i = ULAB_MAX_DIMS - 1; i > 0; i--) {
+ if(i > index) {
+ shape[i] = ndarray->shape[i];
+ strides[i] = ndarray->strides[i];
+ } else {
+ shape[i] = ndarray->shape[i-1];
+ strides[i] = ndarray->strides[i-1];
+ }
+ }
+ }
+
+Once the reduced ``strides`` and ``shape`` are known, we place the axis
+in question in the innermost loop, and wrap it with the loops, whose
+coordinates are in the ``strides``, and ``shape`` arrays. The
+``RUN_STD`` macro from
+`numerical.h <https://github.com/v923z/micropython-ulab/blob/master/code/numpy/numerical/numerical.h>`__
+is a good example. The macro is expanded in the
+``numerical_sum_mean_std_ndarray`` function.
+
+.. code:: c
+
+ static mp_obj_t numerical_sum_mean_std_ndarray(ndarray_obj_t *ndarray, mp_obj_t axis, uint8_t optype, size_t ddof) {
+ uint8_t *array = (uint8_t *)ndarray->array;
+ size_t *shape = m_new(size_t, ULAB_MAX_DIMS);
+ memset(shape, 0, sizeof(size_t)*ULAB_MAX_DIMS);
+ int32_t *strides = m_new(int32_t, ULAB_MAX_DIMS);
+ memset(strides, 0, sizeof(uint32_t)*ULAB_MAX_DIMS);
+
+ int8_t ax = mp_obj_get_int(axis);
+ if(ax < 0) ax += ndarray->ndim;
+ if((ax < 0) || (ax > ndarray->ndim - 1)) {
+ mp_raise_ValueError(translate("index out of range"));
+ }
+ numerical_reduce_axes(ndarray, ax, shape, strides);
+ uint8_t index = ULAB_MAX_DIMS - ndarray->ndim + ax;
+ ndarray_obj_t *results = NULL;
+ uint8_t *rarray = NULL;
+ ...
+
+Here is the macro for the three-dimensional case:
+
+.. code:: c
+
+ #define RUN_STD(ndarray, type, array, results, r, shape, strides, index, div) do {
+ size_t k = 0;
+ do {
+ size_t l = 0;
+ do {
+ RUN_STD1((ndarray), type, (array), (results), (r), (index), (div));
+ (array) -= (ndarray)->strides[(index)] * (ndarray)->shape[(index)];
+ (array) += (strides)[ULAB_MAX_DIMS - 1];
+ l++;
+ } while(l < (shape)[ULAB_MAX_DIMS - 1]);
+ (array) -= (strides)[ULAB_MAX_DIMS - 2] * (shape)[ULAB_MAX_DIMS-2];
+ (array) += (strides)[ULAB_MAX_DIMS - 3];
+ k++;
+ } while(k < (shape)[ULAB_MAX_DIMS - 2]);
+ } while(0)
+
+In ``RUN_STD``, we simply move our pointers; the calculation itself
+happens in the ``RUN_STD1`` macro below. (Note that this is the
+implementation of the numerically stable Welford algorithm.)
+
+.. code:: c
+
+ #define RUN_STD1(ndarray, type, array, results, r, index, div)
+ ({
+ mp_float_t M, m, S = 0.0, s = 0.0;
+ M = m = *(mp_float_t *)((type *)(array));
+ for(size_t i=1; i < (ndarray)->shape[(index)]; i++) {
+ (array) += (ndarray)->strides[(index)];
+ mp_float_t value = *(mp_float_t *)((type *)(array));
+ m = M + (value - M) / (mp_float_t)i;
+ s = S + (value - M) * (value - m);
+ M = m;
+ S = s;
+ }
+ (array) += (ndarray)->strides[(index)];
+ *(r)++ = MICROPY_FLOAT_C_FUN(sqrt)((ndarray)->shape[(index)] * s / (div));
+ })
+
+Upcasting
+---------
+
+When in an operation the ``dtype``\ s of two arrays are different, the
+result’s ``dtype`` will be decided by the following upcasting rules:
+
+1. Operations with two ``ndarray``\ s of the same ``dtype`` preserve
+ their ``dtype``, even when the results overflow.
+
+2. if either of the operands is a float, the result automatically
+ becomes a float
+
+3. otherwise
+
+ - ``uint8`` + ``int8`` => ``int16``,
+
+ - ``uint8`` + ``int16`` => ``int16``
+
+ - ``uint8`` + ``uint16`` => ``uint16``
+
+ - ``int8`` + ``int16`` => ``int16``
+
+ - ``int8`` + ``uint16`` => ``uint16`` (in numpy, the result is a
+ ``int32``)
+
+ - ``uint16`` + ``int16`` => ``float`` (in numpy, the result is a
+ ``int32``)
+
+4. When one operand of a binary operation is a generic scalar
+ ``micropython`` variable, i.e., ``mp_obj_int``, or ``mp_obj_float``,
+ it will be converted to a linear array of length 1, and with the
+ smallest ``dtype`` that can accommodate the variable in question.
+ After that the broadcasting rules apply, as described in the section
+ `Iterating over two ndarrays simultaneously:
+ broadcasting <#Iterating_over_two_ndarrays_simultaneously:_broadcasting>`__
+
+Upcasting is resolved in place, wherever it is required. Notable
+examples can be found in
+`ndarray_operators.c <https://github.com/v923z/micropython-ulab/blob/master/code/ndarray_operators.c>`__
+
+Slicing and indexing
+--------------------
+
+An ``ndarray`` can be indexed with three types of objects: integer
+scalars, slices, and another ``ndarray``, whose elements are either
+integer scalars, or Booleans. Since slice and integer indices can be
+thought of as modifications of the ``strides``, these indices return a
+view of the ``ndarray``. This statement does not hold for ``ndarray``
+indices, and therefore, the return a copy of the array.
+
+Extending ulab
+--------------
+
+The ``user`` module is disabled by default, as can be seen from the last
+couple of lines of
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__
+
+.. code:: c
+
+ // user-defined module
+ #ifndef ULAB_USER_MODULE
+ #define ULAB_USER_MODULE (0)
+ #endif
+
+The module contains a very simple function, ``user_dummy``, and this
+function is bound to the module itself. In other words, even if the
+module is enabled, one has to ``import``:
+
+.. code:: python
+
+
+ import ulab
+ from ulab import user
+
+ user.dummy_function(2.5)
+
+which should just return 5.0. Even if ``numpy``-compatibility is
+required (i.e., if most functions are bound at the top level to ``ulab``
+directly), having to ``import`` the module has a great advantage.
+Namely, only the
+`user.h <https://github.com/v923z/micropython-ulab/blob/master/code/user/user.h>`__
+and
+`user.c <https://github.com/v923z/micropython-ulab/blob/master/code/user/user.c>`__
+files have to be modified, thus it should be relatively straightforward
+to update your local copy from
+`github <https://github.com/v923z/micropython-ulab/blob/master/>`__.
+
+Now, let us see, how we can add a more meaningful function.
+
+Creating a new ndarray
+----------------------
+
+In the `General comments <#General_comments>`__ sections we have seen
+the type definition of an ``ndarray``. This structure can be generated
+by means of a couple of functions listed in
+`ndarray.c <https://github.com/v923z/micropython-ulab/blob/master/code/ndarray.c>`__.
+
+ndarray_new_ndarray
+~~~~~~~~~~~~~~~~~~~
+
+The ``ndarray_new_ndarray`` functions is called by all other
+array-generating functions. It takes the number of dimensions, ``ndim``,
+a ``uint8_t``, the ``shape``, a pointer to ``size_t``, the ``strides``,
+a pointer to ``int32_t``, and ``dtype``, another ``uint8_t`` as its
+arguments, and returns a new array with all entries initialised to 0.
+
+Assuming that ``ULAB_MAX_DIMS > 2``, a new dense array of dimension 3,
+of ``shape`` (3, 4, 5), of ``strides`` (1000, 200, 10), and ``dtype``
+``uint16_t`` can be generated by the following instructions
+
+.. code:: c
+
+ size_t *shape = m_new(size_t, ULAB_MAX_DIMS);
+ shape[ULAB_MAX_DIMS - 1] = 5;
+ shape[ULAB_MAX_DIMS - 2] = 4;
+ shape[ULAB_MAX_DIMS - 3] = 3;
+
+ int32_t *strides = m_new(int32_t, ULAB_MAX_DIMS);
+ strides[ULAB_MAX_DIMS - 1] = 10;
+ strides[ULAB_MAX_DIMS - 2] = 200;
+ strides[ULAB_MAX_DIMS - 3] = 1000;
+
+ ndarray_obj_t *new_ndarray = ndarray_new_ndarray(3, shape, strides, NDARRAY_UINT16);
+
+ndarray_new_dense_ndarray
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The functions simply calculates the ``strides`` from the ``shape``, and
+calls ``ndarray_new_ndarray``. Assuming that ``ULAB_MAX_DIMS > 2``, a
+new dense array of dimension 3, of ``shape`` (3, 4, 5), and ``dtype``
+``mp_float_t`` can be generated by the following instructions
+
+.. code:: c
+
+ size_t *shape = m_new(size_t, ULAB_MAX_DIMS);
+ shape[ULAB_MAX_DIMS - 1] = 5;
+ shape[ULAB_MAX_DIMS - 2] = 4;
+ shape[ULAB_MAX_DIMS - 3] = 3;
+
+ ndarray_obj_t *new_ndarray = ndarray_new_dense_ndarray(3, shape, NDARRAY_FLOAT);
+
+ndarray_new_linear_array
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since the dimensions of a linear array are known (1), the
+``ndarray_new_linear_array`` takes the ``length``, a ``size_t``, and the
+``dtype``, an ``uint8_t``. Internally, ``ndarray_new_linear_array``
+generates the ``shape`` array, and calls ``ndarray_new_dense_array``
+with ``ndim = 1``.
+
+A linear array of length 100, and ``dtype`` ``uint8`` could be created
+by the function call
+
+.. code:: c
+
+ ndarray_obj_t *new_ndarray = ndarray_new_linear_array(100, NDARRAY_UINT8)
+
+ndarray_new_ndarray_from_tuple
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This function takes a ``tuple``, which should hold the lengths of the
+axes (in other words, the ``shape``), and the ``dtype``, and calls
+internally ``ndarray_new_dense_array``. A new ``ndarray`` can be
+generated by calling
+
+.. code:: c
+
+ ndarray_obj_t *new_ndarray = ndarray_new_ndarray_from_tuple(shape, NDARRAY_FLOAT);
+
+where ``shape`` is a tuple.
+
+ndarray_new_view
+~~~~~~~~~~~~~~~~
+
+This function crates a *view*, and takes the source, an ``ndarray``, the
+number of dimensions, an ``uint8_t``, the ``shape``, a pointer to
+``size_t``, the ``strides``, a pointer to ``int32_t``, and the offset,
+an ``int32_t`` as arguments. The offset is the number of bytes by which
+the void ``array`` pointer is shifted. E.g., the ``python`` statement
+
+.. code:: python
+
+ a = np.array([0, 1, 2, 3, 4, 5], dtype=uint8)
+ b = a[1::2]
+
+produces the array
+
+.. code:: python
+
+ array([1, 3, 5], dtype=uint8)
+
+which holds its data at position ``x0 + 1``, if ``a``\ ’s pointer is at
+``x0``. In this particular case, the offset is 1.
+
+The array ``b`` from the example above could be generated as
+
+.. code:: c
+
+ size_t *shape = m_new(size_t, ULAB_MAX_DIMS);
+ shape[ULAB_MAX_DIMS - 1] = 3;
+
+ int32_t *strides = m_new(int32_t, ULAB_MAX_DIMS);
+ strides[ULAB_MAX_DIMS - 1] = 2;
+
+ int32_t offset = 1;
+ uint8_t ndim = 1;
+
+ ndarray_obj_t *new_ndarray = ndarray_new_view(ndarray_a, ndim, shape, strides, offset);
+
+ndarray_copy_array
+~~~~~~~~~~~~~~~~~~
+
+The ``ndarray_copy_array`` function can be used for copying the contents
+of an array. Note that the target array has to be created beforehand.
+E.g., a one-to-one copy can be gotten by
+
+.. code:: c
+
+ ndarray_obj_t *new_ndarray = ndarray_new_ndarray(source->ndim, source->shape, source->strides, source->dtype);
+ ndarray_copy_array(source, new_ndarray);
+
+Note that the function cannot be used for forcing type conversion, i.e.,
+the input and output types must be identical, because the function
+simply calls the ``memcpy`` function. On the other hand, the input and
+output ``strides`` do not necessarily have to be equal.
+
+ndarray_copy_view
+~~~~~~~~~~~~~~~~~
+
+The ``ndarray_obj_t *new_ndarray = ...`` instruction can be saved by
+calling the ``ndarray_copy_view`` function with the single ``source``
+argument.
+
+Accessing data in the ndarray
+-----------------------------
+
+Having seen, how arrays can be generated and copied, it is time to look
+at how the data in an ``ndarray`` can be accessed and modified.
+
+For starters, let us suppose that the object in question comes from the
+user (i.e., via the ``micropython`` interface), First, we have to
+acquire a pointer to the ``ndarray`` by calling
+
+.. code:: c
+
+ ndarray_obj_t *ndarray = MP_OBJ_TO_PTR(object_in);
+
+If it is not clear, whether the object is an ``ndarray`` (e.g., if we
+want to write a function that can take ``ndarray``\ s, and other
+iterables as its argument), we find this out by evaluating
+
+.. code:: c
+
+ mp_obj_is_type(object_in, &ulab_ndarray_type)
+
+which should return ``true``. Once the pointer is at our disposal, we
+can get a pointer to the underlying numerical array as discussed
+earlier, i.e.,
+
+.. code:: c
+
+ uint8_t *array = (uint8_t *)ndarray->array;
+
+If you need to find out the ``dtype`` of the array, you can get it by
+accessing the ``dtype`` member of the ``ndarray``, i.e.,
+
+.. code:: c
+
+ ndarray->dtype
+
+should be equal to ``B``, ``b``, ``H``, ``h``, or ``f``. The size of a
+single item is stored in the ``itemsize`` member. This number should be
+equal to 1, if the ``dtype`` is ``B``, or ``b``, 2, if the ``dtype`` is
+``H``, or ``h``, 4, if the ``dtype`` is ``f``, and 8 for ``d``.
+
+Boilerplate
+-----------
+
+In the next section, we will construct a function that generates the
+element-wise square of a dense array, otherwise, raises a ``TypeError``
+exception. Dense arrays can easily be iterated over, since we do not
+have to care about the ``shape`` and the ``strides``. If the array is
+sparse, the section `Iterating over elements of a
+tensor <#Iterating-over-elements-of-a-tensor>`__ should contain hints as
+to how the iteration can be implemented.
+
+The function is listed under
+`user.c <https://github.com/v923z/micropython-ulab/tree/master/code/user/>`__.
+The ``user`` module is bound to ``ulab`` in
+`ulab.c <https://github.com/v923z/micropython-ulab/tree/master/code/ulab.c>`__
+in the lines
+
+.. code:: c
+
+ #if ULAB_USER_MODULE
+ { MP_ROM_QSTR(MP_QSTR_user), MP_ROM_PTR(&ulab_user_module) },
+ #endif
+
+which assumes that at the very end of
+`ulab.h <https://github.com/v923z/micropython-ulab/tree/master/code/ulab.h>`__
+the
+
+.. code:: c
+
+ // user-defined module
+ #ifndef ULAB_USER_MODULE
+ #define ULAB_USER_MODULE (1)
+ #endif
+
+constant has been set to 1. After compilation, you can call a particular
+``user`` function in ``python`` by importing the module first, i.e.,
+
+.. code:: python
+
+ from ulab import numpy as np
+ from ulab import user
+
+ user.some_function(...)
+
+This separation of user-defined functions from the rest of the code
+ensures that the integrity of the main module and all its functions are
+always preserved. Even in case of a catastrophic failure, you can
+exclude the ``user`` module, and start over.
+
+And now the function:
+
+.. code:: c
+
+ static mp_obj_t user_square(mp_obj_t arg) {
+ // the function takes a single dense ndarray, and calculates the
+ // element-wise square of its entries
+
+ // raise a TypeError exception, if the input is not an ndarray
+ if(!mp_obj_is_type(arg, &ulab_ndarray_type)) {
+ mp_raise_TypeError(translate("input must be an ndarray"));
+ }
+ ndarray_obj_t *ndarray = MP_OBJ_TO_PTR(arg);
+
+ // make sure that the input is a dense array
+ if(!ndarray_is_dense(ndarray)) {
+ mp_raise_TypeError(translate("input must be a dense ndarray"));
+ }
+
+ // if the input is a dense array, create `results` with the same number of
+ // dimensions, shape, and dtype
+ ndarray_obj_t *results = ndarray_new_dense_ndarray(ndarray->ndim, ndarray->shape, ndarray->dtype);
+
+ // since in a dense array the iteration over the elements is trivial, we
+ // can cast the data arrays ndarray->array and results->array to the actual type
+ if(ndarray->dtype == NDARRAY_UINT8) {
+ uint8_t *array = (uint8_t *)ndarray->array;
+ uint8_t *rarray = (uint8_t *)results->array;
+ for(size_t i=0; i < ndarray->len; i++, array++) {
+ *rarray++ = (*array) * (*array);
+ }
+ } else if(ndarray->dtype == NDARRAY_INT8) {
+ int8_t *array = (int8_t *)ndarray->array;
+ int8_t *rarray = (int8_t *)results->array;
+ for(size_t i=0; i < ndarray->len; i++, array++) {
+ *rarray++ = (*array) * (*array);
+ }
+ } else if(ndarray->dtype == NDARRAY_UINT16) {
+ uint16_t *array = (uint16_t *)ndarray->array;
+ uint16_t *rarray = (uint16_t *)results->array;
+ for(size_t i=0; i < ndarray->len; i++, array++) {
+ *rarray++ = (*array) * (*array);
+ }
+ } else if(ndarray->dtype == NDARRAY_INT16) {
+ int16_t *array = (int16_t *)ndarray->array;
+ int16_t *rarray = (int16_t *)results->array;
+ for(size_t i=0; i < ndarray->len; i++, array++) {
+ *rarray++ = (*array) * (*array);
+ }
+ } else { // if we end up here, the dtype is NDARRAY_FLOAT
+ mp_float_t *array = (mp_float_t *)ndarray->array;
+ mp_float_t *rarray = (mp_float_t *)results->array;
+ for(size_t i=0; i < ndarray->len; i++, array++) {
+ *rarray++ = (*array) * (*array);
+ }
+ }
+ // at the end, return a micropython object
+ return MP_OBJ_FROM_PTR(results);
+ }
+
+To summarise, the steps for *implementing* a function are
+
+1. If necessary, inspect the type of the input object, which is always a
+ ``mp_obj_t`` object
+2. If the input is an ``ndarray_obj_t``, acquire a pointer to it by
+ calling ``ndarray_obj_t *ndarray = MP_OBJ_TO_PTR(arg);``
+3. Create a new array, or modify the existing one; get a pointer to the
+ data by calling ``uint8_t *array = (uint8_t *)ndarray->array;``, or
+ something equivalent
+4. Once the new data have been calculated, return a ``micropython``
+ object by calling ``MP_OBJ_FROM_PTR(...)``.
+
+The listing above contains the implementation of the function, but as
+such, it cannot be called from ``python``: it still has to be bound to
+the name space. This we do by first defining a function object in
+
+.. code:: c
+
+ MP_DEFINE_CONST_FUN_OBJ_1(user_square_obj, user_square);
+
+``micropython`` defines a number of ``MP_DEFINE_CONST_FUN_OBJ_N`` macros
+in
+`obj.h <https://github.com/micropython/micropython/blob/master/py/obj.h>`__.
+``N`` is always the number of arguments the function takes. We had a
+function definition ``static mp_obj_t user_square(mp_obj_t arg)``, i.e.,
+we dealt with a single argument.
+
+Finally, we have to bind this function object in the globals table of
+the ``user`` module:
+
+.. code:: c
+
+ STATIC const mp_rom_map_elem_t ulab_user_globals_table[] = {
+ { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_user) },
+ { MP_OBJ_NEW_QSTR(MP_QSTR_square), (mp_obj_t)&user_square_obj },
+ };
+
+Thus, the three steps required for the definition of a user-defined
+function are
+
+1. The low-level implementation of the function itself
+2. The definition of a function object by calling
+ MP_DEFINE_CONST_FUN_OBJ_N()
+3. Binding this function object to the namespace in the
+ ``ulab_user_globals_table[]``
diff --git a/circuitpython/extmod/ulab/docs/manual/source/ulab-tricks.rst b/circuitpython/extmod/ulab/docs/manual/source/ulab-tricks.rst
new file mode 100644
index 0000000..4c3802b
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/ulab-tricks.rst
@@ -0,0 +1,268 @@
+
+Tricks
+======
+
+This section of the book discusses a couple of tricks that can be
+exploited to either speed up computations, or save on RAM. However,
+there is probably no silver bullet, and you have to evaluate your code
+in terms of execution speed (if the execution is time critical), or RAM
+used. You should also keep in mind that, if a particular code snippet is
+optimised on some hardware, there is no guarantee that on another piece
+of hardware, you will get similar improvements. Hardware implementations
+are vastly different. Some microcontrollers do not even have an FPU, so
+you should not be surprised that you get significantly different
+benchmarks. Just to underline this statement, you can study the
+`collection of benchmarks <https://github.com/thiagofe/ulab_samples>`__.
+
+Use an ``ndarray``, if you can
+------------------------------
+
+Many functions in ``ulab`` are implemented in a universal fashion,
+meaning that both generic ``micropython`` iterables, and ``ndarray``\ s
+can be passed as an argument. E.g., both
+
+.. code:: python
+
+ from ulab import numpy as np
+
+ np.sum([1, 2, 3, 4, 5])
+
+and
+
+.. code:: python
+
+ from ulab import numpy as np
+
+ a = np.array([1, 2, 3, 4, 5])
+ np.sum(a)
+
+will return the ``micropython`` variable 15 as the result. Still,
+``np.sum(a)`` is evaluated significantly faster, because in
+``np.sum([1, 2, 3, 4, 5])``, the interpreter has to fetch 5
+``micropython`` variables, convert them to ``float``, and sum the
+values, while the C type of ``a`` is known, thus the interpreter can
+invoke a single ``for`` loop for the evaluation of the ``sum``. In the
+``for`` loop, there are no function calls, the iteration simply walks
+through the pointer holding the values of ``a``, and adds the values to
+an accumulator. If the array ``a`` is already available, then you can
+gain a factor of 3 in speed by calling ``sum`` on the array, instead of
+using the list. Compared to the python implementation of the same
+functionality, the speed-up is around 40 (again, this might depend on
+the hardware).
+
+On the other hand, if the array is not available, then there is not much
+point in converting the list to an ``ndarray`` and passing that to the
+function. In fact, you should expect a slow-down: the constructor has to
+iterate over the list elements, and has to convert them to a numerical
+type. On top of that, it also has to reserve RAM for the ``ndarray``.
+
+Use a reasonable ``dtype``
+--------------------------
+
+Just as in ``numpy``, the default ``dtype`` is ``float``. But this does
+not mean that that is the most suitable one in all scenarios. If data
+are streamed from an 8-bit ADC, and you only want to know the maximum,
+or the sum, then it is quite reasonable to use ``uint8`` for the
+``dtype``. Storing the same data in ``float`` array would cost 4 or 8
+times as much RAM, with absolutely no gain. Do not rely on the default
+value of the constructor’s keyword argument, and choose one that fits!
+
+Beware the axis!
+----------------
+
+Whenever ``ulab`` iterates over multi-dimensional arrays, the outermost
+loop is the first axis, then the second axis, and so on. E.g., when the
+``sum`` of
+
+.. code:: python
+
+ a = array([[1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12]], dtype=uint8)
+
+is being calculated, first the data pointer walks along ``[1, 2, 3, 4]``
+(innermost loop, last axis), then is moved back to the position, where 5
+is stored (this is the nesting loop), and traverses ``[5, 6, 7, 8]``,
+and so on. Moving the pointer back to 5 is more expensive, than moving
+it along an axis, because the position of 5 has to be calculated,
+whereas moving from 5 to 6 is simply an addition to the address. Thus,
+while the matrix
+
+.. code:: python
+
+ b = array([[1, 5, 9],
+ [2, 6, 10],
+ [3, 7, 11],
+ [4, 8, 12]], dtype=uint8)
+
+holds the same data as ``a``, the summation over the entries in ``b`` is
+slower, because the pointer has to be re-wound three times, as opposed
+to twice in ``a``. For small matrices the savings are not significant,
+but you would definitely notice the difference, if you had
+
+::
+
+ a = array(range(2000)).reshape((2, 1000))
+ b = array(range(2000)).reshape((1000, 2))
+
+The moral is that, in order to improve on the execution speed, whenever
+possible, you should try to make the last axis the longest. As a side
+note, ``numpy`` can re-arrange its loops, and puts the longest axis in
+the innermost loop. This is why the longest axis is sometimes referred
+to as the fast axis. In ``ulab``, the order of the axes is fixed.
+
+Reduce the number of artifacts
+------------------------------
+
+Before showing a real-life example, let us suppose that we want to
+interpolate uniformly sampled data, and the absolute magnitude is not
+really important, we only care about the ratios between neighbouring
+value. One way of achieving this is calling the ``interp`` functions.
+However, we could just as well work with slices.
+
+.. code::
+
+ # code to be run in CPython
+
+ a = array([0, 10, 2, 20, 4], dtype=np.uint8)
+ b = np.zeros(9, dtype=np.uint8)
+
+ b[::2] = 2 * a
+ b[1::2] = a[:-1] + a[1:]
+
+ b //= 2
+ b
+
+
+
+.. parsed-literal::
+
+ array([ 0, 5, 10, 6, 2, 11, 20, 12, 4], dtype=uint8)
+
+
+
+``b`` now has values from ``a`` at every even position, and interpolates
+the values on every odd position. If only the relative magnitudes are
+important, then we can even save the division by 2, and we end up with
+
+.. code::
+
+ # code to be run in CPython
+
+ a = array([0, 10, 2, 20, 4], dtype=np.uint8)
+ b = np.zeros(9, dtype=np.uint8)
+
+ b[::2] = 2 * a
+ b[1::2] = a[:-1] + a[1:]
+
+ b
+
+
+
+.. parsed-literal::
+
+ array([ 0, 10, 20, 12, 4, 22, 40, 24, 8], dtype=uint8)
+
+
+
+Importantly, we managed to keep the results in the smaller ``dtype``,
+``uint8``. Now, while the two assignments above are terse and pythonic,
+the code is not the most efficient: the right hand sides are compound
+statements, generating intermediate results. To store them, RAM has to
+be allocated. This takes time, and leads to memory fragmentation. Better
+is to write out the assignments in 4 instructions:
+
+.. code::
+
+ # code to be run in CPython
+
+ b = np.zeros(9, dtype=np.uint8)
+
+ b[::2] = a
+ b[::2] += a
+ b[1::2] = a[:-1]
+ b[1::2] += a[1:]
+
+ b
+
+
+
+.. parsed-literal::
+
+ array([ 0, 10, 20, 12, 4, 22, 40, 24, 8], dtype=uint8)
+
+
+
+The results are the same, but no extra RAM is allocated, except for the
+views ``a[:-1]``, and ``a[1:]``, but those had to be created even in the
+origin implementation.
+
+Upscaling images
+~~~~~~~~~~~~~~~~
+
+And now the example: there are low-resolution thermal cameras out there.
+Low resolution might mean 8 by 8 pixels. Such a small number of pixels
+is just not reasonable to plot, no matter how small the display is. If
+you want to make the camera image a bit more pleasing, you can upscale
+(stretch) it in both dimensions. This can be done exactly as we
+up-scaled the linear array:
+
+.. code::
+
+ # code to be run in CPython
+
+ b = np.zeros((15, 15), dtype=np.uint8)
+
+ b[1::2,::2] = a[:-1,:]
+ b[1::2,::2] += a[1:, :]
+ b[1::2,::2] //= 2
+ b[::,1::2] = a[::,:-1:2]
+ b[::,1::2] += a[::,2::2]
+ b[::,1::2] //= 2
+Up-scaling by larger numbers can be done in a similar fashion, you
+simply have more assignments.
+
+There are cases, when one cannot do away with the intermediate results.
+Two prominent cases are the ``where`` function, and indexing by means of
+a Boolean array. E.g., in
+
+.. code::
+
+ # code to be run in CPython
+
+ a = array([1, 2, 3, 4, 5])
+ b = a[a < 4]
+ b
+
+
+
+.. parsed-literal::
+
+ array([1, 2, 3])
+
+
+
+the expression ``a < 4`` produces the Boolean array,
+
+.. code::
+
+ # code to be run in CPython
+
+ a < 4
+
+
+
+.. parsed-literal::
+
+ array([ True, True, True, False, False])
+
+
+
+If you repeatedly have such conditions in a loop, you might have to
+peridically call the garbage collector to remove the Boolean arrays that
+are used only once.
+
+.. code::
+
+ # code to be run in CPython
+
diff --git a/circuitpython/extmod/ulab/docs/manual/source/ulab-utils.rst b/circuitpython/extmod/ulab/docs/manual/source/ulab-utils.rst
new file mode 100644
index 0000000..15cf978
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/manual/source/ulab-utils.rst
@@ -0,0 +1,143 @@
+
+ulab utilities
+==============
+
+There might be cases, when the format of your data does not conform to
+``ulab``, i.e., there is no obvious way to map the data to any of the
+five supported ``dtype``\ s. A trivial example is an ADC or microphone
+signal with 32-bit resolution. For such cases, ``ulab`` defines the
+``utils`` module, which, at the moment, has four functions that are not
+``numpy`` compatible, but which should ease interfacing ``ndarray``\ s
+to peripheral devices.
+
+The ``utils`` module can be enabled by setting the
+``ULAB_HAS_UTILS_MODULE`` constant to 1 in
+`ulab.h <https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h>`__:
+
+.. code:: c
+
+ #ifndef ULAB_HAS_UTILS_MODULE
+ #define ULAB_HAS_UTILS_MODULE (1)
+ #endif
+
+This still does not compile any functions into the firmware. You can add
+a function by setting the corresponding pre-processor constant to 1.
+E.g.,
+
+.. code:: c
+
+ #ifndef ULAB_UTILS_HAS_FROM_INT16_BUFFER
+ #define ULAB_UTILS_HAS_FROM_INT16_BUFFER (1)
+ #endif
+
+from_int32_buffer, from_uint32_buffer
+-------------------------------------
+
+With the help of ``utils.from_int32_buffer``, and
+``utils.from_uint32_buffer``, it is possible to convert 32-bit integer
+buffers to ``ndarrays`` of float type. These functions have a syntax
+similar to ``numpy.frombuffer``; they support the ``count=-1``, and
+``offset=0`` keyword arguments. However, in addition, they also accept
+``out=None``, and ``byteswap=False``.
+
+Here is an example without keyword arguments
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import utils
+
+ a = bytearray([1, 1, 0, 0, 0, 0, 0, 255])
+ print('a: ', a)
+ print()
+ print('unsigned integers: ', utils.from_uint32_buffer(a))
+
+ b = bytearray([1, 1, 0, 0, 0, 0, 0, 255])
+ print('\nb: ', b)
+ print()
+ print('signed integers: ', utils.from_int32_buffer(b))
+
+.. parsed-literal::
+
+ a: bytearray(b'\x01\x01\x00\x00\x00\x00\x00\xff')
+
+ unsigned integers: array([257.0, 4278190080.000001], dtype=float64)
+
+ b: bytearray(b'\x01\x01\x00\x00\x00\x00\x00\xff')
+
+ signed integers: array([257.0, -16777216.0], dtype=float64)
+
+
+
+
+The meaning of ``count``, and ``offset`` is similar to that in
+``numpy.frombuffer``. ``count`` is the number of floats that will be
+converted, while ``offset`` would discard the first ``offset`` number of
+bytes from the buffer before the conversion.
+
+In the example above, repeated calls to either of the functions returns
+a new ``ndarray``. You can save RAM by supplying the ``out`` keyword
+argument with a pre-defined ``ndarray`` of sufficient size, in which
+case the results will be inserted into the ``ndarray``. If the ``dtype``
+of ``out`` is not ``float``, a ``TypeError`` exception will be raised.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import utils
+
+ a = np.array([1, 2], dtype=np.float)
+ b = bytearray([1, 0, 1, 0, 0, 1, 0, 1])
+ print('b: ', b)
+ utils.from_uint32_buffer(b, out=a)
+ print('a: ', a)
+
+.. parsed-literal::
+
+ b: bytearray(b'\x01\x00\x01\x00\x00\x01\x00\x01')
+ a: array([65537.0, 16777472.0], dtype=float64)
+
+
+
+
+Finally, since there is no guarantee that the endianness of a particular
+peripheral device supplying the buffer is the same as that of the
+microcontroller, ``from_(u)intbuffer`` allows a conversion via the
+``byteswap`` keyword argument.
+
+.. code::
+
+ # code to be run in micropython
+
+ from ulab import numpy as np
+ from ulab import utils
+
+ a = bytearray([1, 0, 0, 0, 0, 0, 0, 1])
+ print('a: ', a)
+ print('buffer without byteswapping: ', utils.from_uint32_buffer(a))
+ print('buffer with byteswapping: ', utils.from_uint32_buffer(a, byteswap=True))
+
+.. parsed-literal::
+
+ a: bytearray(b'\x01\x00\x00\x00\x00\x00\x00\x01')
+ buffer without byteswapping: array([1.0, 16777216.0], dtype=float64)
+ buffer with byteswapping: array([16777216.0, 1.0], dtype=float64)
+
+
+
+
+from_int16_buffer, from_uint16_buffer
+-------------------------------------
+
+These two functions are identical to ``utils.from_int32_buffer``, and
+``utils.from_uint32_buffer``, with the exception that they convert
+16-bit integers to floating point ``ndarray``\ s.
+
+.. code::
+
+ # code to be run in CPython
+
diff --git a/circuitpython/extmod/ulab/docs/numpy-fft.ipynb b/circuitpython/extmod/ulab/docs/numpy-fft.ipynb
new file mode 100644
index 0000000..803c923
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/numpy-fft.ipynb
@@ -0,0 +1,546 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-01T09:27:13.438054Z",
+ "start_time": "2020-05-01T09:27:13.191491Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:24:48.499467Z",
+ "start_time": "2022-01-07T18:24:48.488004Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-07-23T20:31:25.296014Z",
+ "start_time": "2020-07-23T20:31:25.265937Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# numpy.fft\n",
+ "\n",
+ "Functions related to Fourier transforms can be called by prepending them with `numpy.fft.`. The module defines the following two functions:\n",
+ "\n",
+ "1. [numpy.fft.fft](#fft)\n",
+ "1. [numpy.fft.ifft](#ifft)\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.fft.ifft.html\n",
+ "\n",
+ "## fft\n",
+ "\n",
+ "Since `ulab`'s `ndarray` does not support complex numbers, the invocation of the Fourier transform differs from that in `numpy`. In `numpy`, you can simply pass an array or iterable to the function, and it will be treated as a complex array:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 341,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-17T17:33:38.487729Z",
+ "start_time": "2019-10-17T17:33:38.473515Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([20.+0.j, 0.+0.j, -4.+4.j, 0.+0.j, -4.+0.j, 0.+0.j, -4.-4.j,\n",
+ " 0.+0.j])"
+ ]
+ },
+ "execution_count": 341,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "fft.fft([1, 2, 3, 4, 1, 2, 3, 4])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**WARNING:** The array returned is also complex, i.e., the real and imaginary components are cast together. In `ulab`, the real and imaginary parts are treated separately: you have to pass two `ndarray`s to the function, although, the second argument is optional, in which case the imaginary part is assumed to be zero.\n",
+ "\n",
+ "**WARNING:** The function, as opposed to `numpy`, returns a 2-tuple, whose elements are two `ndarray`s, holding the real and imaginary parts of the transform separately. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 114,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-02-16T18:38:07.294862Z",
+ "start_time": "2020-02-16T18:38:07.233842Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "real part:\t array([5119.996, -5.004663, -5.004798, ..., -5.005482, -5.005643, -5.006577], dtype=float)\r\n",
+ "\r\n",
+ "imaginary part:\t array([0.0, 1631.333, 815.659, ..., -543.764, -815.6588, -1631.333], dtype=float)\r\n",
+ "\r\n",
+ "real part:\t array([5119.996, -5.004663, -5.004798, ..., -5.005482, -5.005643, -5.006577], dtype=float)\r\n",
+ "\r\n",
+ "imaginary part:\t array([0.0, 1631.333, 815.659, ..., -543.764, -815.6588, -1631.333], dtype=float)\r\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.linspace(0, 10, num=1024)\n",
+ "y = np.sin(x)\n",
+ "z = np.zeros(len(x))\n",
+ "\n",
+ "a, b = np.fft.fft(x)\n",
+ "print('real part:\\t', a)\n",
+ "print('\\nimaginary part:\\t', b)\n",
+ "\n",
+ "c, d = np.fft.fft(x, z)\n",
+ "print('\\nreal part:\\t', c)\n",
+ "print('\\nimaginary part:\\t', d)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### ulab with complex support\n",
+ "\n",
+ "If the `ULAB_SUPPORTS_COMPLEX`, and `ULAB_FFT_IS_NUMPY_COMPATIBLE` pre-processor constants are set to 1 in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h) as \n",
+ "\n",
+ "```c\n",
+ "// Adds support for complex ndarrays\n",
+ "#ifndef ULAB_SUPPORTS_COMPLEX\n",
+ "#define ULAB_SUPPORTS_COMPLEX (1)\n",
+ "#endif\n",
+ "```\n",
+ "\n",
+ "```c\n",
+ "#ifndef ULAB_FFT_IS_NUMPY_COMPATIBLE\n",
+ "#define ULAB_FFT_IS_NUMPY_COMPATIBLE (1)\n",
+ "#endif\n",
+ "```\n",
+ "then the FFT routine will behave in a `numpy`-compatible way: the single input array can either be real, in which case the imaginary part is assumed to be zero, or complex. The output is also complex. \n",
+ "\n",
+ "While `numpy`-compatibility might be a desired feature, it has one side effect, namely, the FFT routine consumes approx. 50% more RAM. The reason for this lies in the implementation details."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## ifft\n",
+ "\n",
+ "The above-mentioned rules apply to the inverse Fourier transform. The inverse is also normalised by `N`, the number of elements, as is customary in `numpy`. With the normalisation, we can ascertain that the inverse of the transform is equal to the original array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 459,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-19T13:08:17.647416Z",
+ "start_time": "2019-10-19T13:08:17.597456Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "original vector:\t array([0.0, 0.009775016, 0.0195491, ..., -0.5275068, -0.5357859, -0.5440139], dtype=float)\n",
+ "\n",
+ "real part of inverse:\t array([-2.980232e-08, 0.0097754, 0.0195494, ..., -0.5275064, -0.5357857, -0.5440133], dtype=float)\n",
+ "\n",
+ "imaginary part of inverse:\t array([-2.980232e-08, -1.451171e-07, 3.693752e-08, ..., 6.44871e-08, 9.34986e-08, 2.18336e-07], dtype=float)\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.linspace(0, 10, num=1024)\n",
+ "y = np.sin(x)\n",
+ "\n",
+ "a, b = np.fft.fft(y)\n",
+ "\n",
+ "print('original vector:\\t', y)\n",
+ "\n",
+ "y, z = np.fft.ifft(a, b)\n",
+ "# the real part should be equal to y\n",
+ "print('\\nreal part of inverse:\\t', y)\n",
+ "# the imaginary part should be equal to zero\n",
+ "print('\\nimaginary part of inverse:\\t', z)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that unlike in `numpy`, the length of the array on which the Fourier transform is carried out must be a power of 2. If this is not the case, the function raises a `ValueError` exception."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### ulab with complex support\n",
+ "\n",
+ "The `fft.ifft` function can also be made `numpy`-compatible by setting the `ULAB_SUPPORTS_COMPLEX`, and `ULAB_FFT_IS_NUMPY_COMPATIBLE` pre-processor constants to 1."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Computation and storage costs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### RAM\n",
+ "\n",
+ "The FFT routine of `ulab` calculates the transform in place. This means that beyond reserving space for the two `ndarray`s that will be returned (the computation uses these two as intermediate storage space), only a handful of temporary variables, all floats or 32-bit integers, are required. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Speed of FFTs\n",
+ "\n",
+ "A comment on the speed: a 1024-point transform implemented in python would cost around 90 ms, and 13 ms in assembly, if the code runs on the pyboard, v.1.1. You can gain a factor of four by moving to the D series \n",
+ "https://github.com/peterhinch/micropython-fourier/blob/master/README.md#8-performance. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 494,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-19T13:25:40.540913Z",
+ "start_time": "2019-10-19T13:25:40.509598Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "execution time: 1985 us\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.linspace(0, 10, num=1024)\n",
+ "y = np.sin(x)\n",
+ "\n",
+ "@timeit\n",
+ "def np_fft(y):\n",
+ " return np.fft.fft(y)\n",
+ "\n",
+ "a, b = np_fft(y)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The C implementation runs in less than 2 ms on the pyboard (we have just measured that), and has been reported to run in under 0.8 ms on the D series board. That is an improvement of at least a factor of four. "
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/numpy-functions.ipynb b/circuitpython/extmod/ulab/docs/numpy-functions.ipynb
new file mode 100644
index 0000000..f115a41
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/numpy-functions.ipynb
@@ -0,0 +1,2393 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-13T08:28:06.727371Z",
+ "start_time": "2021-02-13T08:28:04.925338Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:45:28.079350Z",
+ "start_time": "2022-01-07T19:45:28.073911Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:45:28.654136Z",
+ "start_time": "2022-01-07T19:45:28.634610Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Numpy functions"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This section of the manual discusses those functions that were adapted from `numpy`. Starred functions accept complex arrays as arguments, if the firmware was compiled with complex support.\n",
+ "\n",
+ "1. [numpy.all*](#all)\n",
+ "1. [numpy.any*](#any)\n",
+ "1. [numpy.argmax](#argmax)\n",
+ "1. [numpy.argmin](#argmin)\n",
+ "1. [numpy.argsort](#argsort)\n",
+ "1. [numpy.clip](#clip)\n",
+ "1. [numpy.compress*](#compress)\n",
+ "1. [numpy.conjugate*](#conjugate)\n",
+ "1. [numpy.convolve*](#convolve)\n",
+ "1. [numpy.diff](#diff)\n",
+ "1. [numpy.dot](#dot)\n",
+ "1. [numpy.equal](#equal)\n",
+ "1. [numpy.flip*](#flip)\n",
+ "1. [numpy.imag*](#imag)\n",
+ "1. [numpy.interp](#interp)\n",
+ "1. [numpy.isfinite](#isfinite)\n",
+ "1. [numpy.isinf](#isinf)\n",
+ "1. [numpy.max](#max)\n",
+ "1. [numpy.maximum](#maximum)\n",
+ "1. [numpy.mean](#mean)\n",
+ "1. [numpy.median](#median)\n",
+ "1. [numpy.min](#min)\n",
+ "1. [numpy.minimum](#minimum)\n",
+ "1. [numpy.not_equal](#equal)\n",
+ "1. [numpy.polyfit](#polyfit)\n",
+ "1. [numpy.polyval](#polyval)\n",
+ "1. [numpy.real*](#real)\n",
+ "1. [numpy.roll](#roll)\n",
+ "1. [numpy.sort](#sort)\n",
+ "1. [numpy.sort_complex*](#sort_complex)\n",
+ "1. [numpy.std](#std)\n",
+ "1. [numpy.sum](#sum)\n",
+ "1. [numpy.trace](#trace)\n",
+ "1. [numpy.trapz](#trapz)\n",
+ "1. [numpy.where](#where)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## all\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.all.html\n",
+ "\n",
+ "The function takes one positional, and one keyword argument, the `axis`, with a default value of `None`, and tests, whether *all* array elements along the given axis evaluate to `True`. If the keyword argument is `None`, the flattened array is inspected. \n",
+ "\n",
+ "Elements of an array evaluate to `True`, if they are not equal to zero, or the Boolean `False`. The return value if a Boolean `ndarray`.\n",
+ "\n",
+ "If the firmware was compiled with complex support, the function can accept complex arrays."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-08T16:54:57.117630Z",
+ "start_time": "2021-02-08T16:54:57.105337Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[0.0, 1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0]], dtype=float64)\n",
+ "\n",
+ "all of the flattened array:\n",
+ " False\n",
+ "\n",
+ "all of a along 0th axis:\n",
+ " array([False, True, True, True], dtype=bool)\n",
+ "\n",
+ "all of a along 1st axis:\n",
+ " array([False, True, True], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(12)).reshape((3, 4))\n",
+ "\n",
+ "print('\\na:\\n', a)\n",
+ "\n",
+ "b = np.all(a)\n",
+ "print('\\nall of the flattened array:\\n', b)\n",
+ "\n",
+ "c = np.all(a, axis=0)\n",
+ "print('\\nall of a along 0th axis:\\n', c)\n",
+ "\n",
+ "d = np.all(a, axis=1)\n",
+ "print('\\nall of a along 1st axis:\\n', d)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## any\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.any.html\n",
+ "\n",
+ "The function takes one positional, and one keyword argument, the `axis`, with a default value of `None`, and tests, whether *any* array element along the given axis evaluates to `True`. If the keyword argument is `None`, the flattened array is inspected. \n",
+ "\n",
+ "Elements of an array evaluate to `True`, if they are not equal to zero, or the Boolean `False`. The return value if a Boolean `ndarray`.\n",
+ "\n",
+ "If the firmware was compiled with complex support, the function can accept complex arrays."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-08T16:54:14.704132Z",
+ "start_time": "2021-02-08T16:54:14.693700Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[0.0, 1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0]], dtype=float64)\n",
+ "\n",
+ "any of the flattened array:\n",
+ " True\n",
+ "\n",
+ "any of a along 0th axis:\n",
+ " array([True, True, True, True], dtype=bool)\n",
+ "\n",
+ "any of a along 1st axis:\n",
+ " array([True, True, True], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(12)).reshape((3, 4))\n",
+ "\n",
+ "print('\\na:\\n', a)\n",
+ "\n",
+ "b = np.any(a)\n",
+ "print('\\nany of the flattened array:\\n', b)\n",
+ "\n",
+ "c = np.any(a, axis=0)\n",
+ "print('\\nany of a along 0th axis:\\n', c)\n",
+ "\n",
+ "d = np.any(a, axis=1)\n",
+ "print('\\nany of a along 1st axis:\\n', d)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## argmax\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
+ "\n",
+ "See [numpy.max](#max)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## argmin\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmin.html\n",
+ "\n",
+ "See [numpy.max](#max)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## argsort\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argsort.html\n",
+ "\n",
+ "Similarly to [sort](#sort), `argsort` takes a positional, and a keyword argument, and returns an unsigned short index array of type `ndarray` with the same dimensions as the input, or, if `axis=None`, as a row vector with length equal to the number of elements in the input (i.e., the flattened array). The indices in the output sort the input in ascending order. The routine in `argsort` is the same as in `sort`, therefore, the comments on computational expenses (time and RAM) also apply. In particular, since no copy of the original data is required, virtually no RAM beyond the output array is used. \n",
+ "\n",
+ "Since the underlying container of the output array is of type `uint16_t`, neither of the output dimensions should be larger than 65535. If that happens to be the case, the function will bail out with a `ValueError`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:33:33.292717Z",
+ "start_time": "2021-01-13T16:33:33.280144Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[1.0, 12.0, 3.0, 0.0],\n",
+ " [5.0, 3.0, 4.0, 1.0],\n",
+ " [9.0, 11.0, 1.0, 8.0],\n",
+ " [7.0, 10.0, 0.0, 1.0]], dtype=float64)\n",
+ "\n",
+ "a sorted along vertical axis:\n",
+ " array([[0, 1, 3, 0],\n",
+ " [1, 3, 2, 1],\n",
+ " [3, 2, 0, 3],\n",
+ " [2, 0, 1, 2]], dtype=uint16)\n",
+ "\n",
+ "a sorted along horizontal axis:\n",
+ " array([[3, 0, 2, 1],\n",
+ " [3, 1, 2, 0],\n",
+ " [2, 3, 0, 1],\n",
+ " [2, 3, 0, 1]], dtype=uint16)\n",
+ "\n",
+ "Traceback (most recent call last):\n",
+ " File \"/dev/shm/micropython.py\", line 12, in <module>\n",
+ "NotImplementedError: argsort is not implemented for flattened arrays\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)\n",
+ "print('\\na:\\n', a)\n",
+ "b = np.argsort(a, axis=0)\n",
+ "print('\\na sorted along vertical axis:\\n', b)\n",
+ "\n",
+ "c = np.argsort(a, axis=1)\n",
+ "print('\\na sorted along horizontal axis:\\n', c)\n",
+ "\n",
+ "c = np.argsort(a, axis=None)\n",
+ "print('\\nflattened a sorted:\\n', c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Since during the sorting, only the indices are shuffled, `argsort` does not modify the input array, as one can verify this by the following example:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:34:48.446211Z",
+ "start_time": "2021-01-13T16:34:48.424276Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([0, 5, 1, 3, 2, 4], dtype=uint8)\n",
+ "\n",
+ "sorting indices:\n",
+ " array([0, 2, 4, 3, 5, 1], dtype=uint16)\n",
+ "\n",
+ "the original array:\n",
+ " array([0, 5, 1, 3, 2, 4], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([0, 5, 1, 3, 2, 4], dtype=np.uint8)\n",
+ "print('\\na:\\n', a)\n",
+ "b = np.argsort(a, axis=0)\n",
+ "print('\\nsorting indices:\\n', b)\n",
+ "print('\\nthe original array:\\n', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## clip\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html\n",
+ "\n",
+ "Clips an array, i.e., values that are outside of an interval are clipped to the interval edges. The function is equivalent to `maximum(a_min, minimum(a, a_max))` broadcasting takes place exactly as in [minimum](#minimum). If the arrays are of different `dtype`, the output is upcast as in [Binary operators](#Binary-operators)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:22:14.147310Z",
+ "start_time": "2021-01-08T13:22:14.123961Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "clipped:\t array([3, 3, 3, 3, 4, 5, 6, 7, 7], dtype=uint8)\n",
+ "\n",
+ "a:\t\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "b:\t\t array([3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=float64)\n",
+ "clipped:\t array([3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 7.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "print('a:\\t\\t', a)\n",
+ "print('clipped:\\t', np.clip(a, 3, 7))\n",
+ "\n",
+ "b = 3 * np.ones(len(a), dtype=np.float)\n",
+ "print('\\na:\\t\\t', a)\n",
+ "print('b:\\t\\t', b)\n",
+ "print('clipped:\\t', np.clip(a, b, 7))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## compress\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.compress.html\n",
+ "\n",
+ "The function returns selected slices of an array along given axis. If the axis keyword is `None`, the flattened array is used.\n",
+ "\n",
+ "If the firmware was compiled with complex support, the function can accept complex arguments."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:51:44.994323Z",
+ "start_time": "2022-01-07T19:51:44.978185Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[0.0, 1.0, 2.0],\n",
+ " [3.0, 4.0, 5.0]], dtype=float64)\n",
+ "\n",
+ "compress(a):\n",
+ " array([[3.0, 4.0, 5.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(6)).reshape((2, 3))\n",
+ "\n",
+ "print('a:\\n', a)\n",
+ "print('\\ncompress(a):\\n', np.compress([0, 1], a, axis=0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## conjugate\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.conjugate.html\n",
+ "\n",
+ "If the firmware was compiled with complex support, the function calculates the complex conjugate of the input array. If the input array is of real `dtype`, then the output is simply a copy, preserving the `dtype`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:30:53.394539Z",
+ "start_time": "2022-01-07T19:30:53.374737Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([1, 2, 3, 4], dtype=uint8)\n",
+ "conjugate(a):\t array([1, 2, 3, 4], dtype=uint8)\n",
+ "\n",
+ "b:\t\t array([1.0+1.0j, 2.0-2.0j, 3.0+3.0j, 4.0-4.0j], dtype=complex)\n",
+ "conjugate(b):\t array([1.0-1.0j, 2.0+2.0j, 3.0-3.0j, 4.0+4.0j], dtype=complex)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
+ "b = np.array([1+1j, 2-2j, 3+3j, 4-4j], dtype=np.complex)\n",
+ "\n",
+ "print('a:\\t\\t', a)\n",
+ "print('conjugate(a):\\t', np.conjugate(a))\n",
+ "print()\n",
+ "print('b:\\t\\t', b)\n",
+ "print('conjugate(b):\\t', np.conjugate(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## convolve\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.convolve.html\n",
+ "\n",
+ "Returns the discrete, linear convolution of two one-dimensional arrays.\n",
+ "\n",
+ "Only the ``full`` mode is supported, and the ``mode`` named parameter is not accepted. Note that all other modes can be had by slicing a ``full`` result.\n",
+ "\n",
+ "If the firmware was compiled with complex support, the function can accept complex arrays."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T15:57:39.028884Z",
+ "start_time": "2021-01-13T15:57:39.008749Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([1.0, 12.0, 123.0, 1230.0, 2300.0, 3000.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.array((1, 2, 3))\n",
+ "y = np.array((1, 10, 100, 1000))\n",
+ "\n",
+ "print(np.convolve(x, y))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## diff\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.diff.html\n",
+ "\n",
+ "The `diff` function returns the numerical derivative of the forward scheme, or more accurately, the differences of an `ndarray` along a given axis. The order of derivative can be stipulated with the `n` keyword argument, which should be between 0, and 9. Default is 1. If higher order derivatives are required, they can be gotten by repeated calls to the function. The `axis` keyword argument should be -1 (last axis, in `ulab` equivalent to the second axis, and this also happens to be the default value), 0, or 1. \n",
+ "\n",
+ "Beyond the output array, the function requires only a couple of bytes of extra RAM for the differentiation stencil. (The stencil is an `int8` array, one byte longer than `n`. This also explains, why the highest order is 9: the coefficients of a ninth-order stencil all fit in signed bytes, while 10 would require `int16`.) Note that as usual in numerical differentiation (and also in `numpy`), the length of the respective axis will be reduced by `n` after the operation. If `n` is larger than, or equal to the length of the axis, an empty array will be returned.\n",
+ "\n",
+ "**WARNING**: the `diff` function does not implement the `prepend` and `append` keywords that can be found in `numpy`. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 106,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-14T16:06:27.468909Z",
+ "start_time": "2021-01-14T16:06:27.439067Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([0, 1, 2, 10, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "\n",
+ "first derivative:\n",
+ " array([1, 1, 8, 250, 1, 1, 1, 1], dtype=uint8)\n",
+ "\n",
+ "second derivative:\n",
+ " array([0, 249, 14, 249, 0, 0, 0], dtype=uint8)\n",
+ "\n",
+ "c:\n",
+ " array([[1.0, 2.0, 3.0, 4.0],\n",
+ " [4.0, 3.0, 2.0, 1.0],\n",
+ " [1.0, 4.0, 9.0, 16.0],\n",
+ " [0.0, 0.0, 0.0, 0.0]], dtype=float64)\n",
+ "\n",
+ "first derivative, first axis:\n",
+ " array([[3.0, 1.0, -1.0, -3.0],\n",
+ " [-3.0, 1.0, 7.0, 15.0],\n",
+ " [-1.0, -4.0, -9.0, -16.0]], dtype=float64)\n",
+ "\n",
+ "first derivative, second axis:\n",
+ " array([[1.0, 1.0, 1.0],\n",
+ " [-1.0, -1.0, -1.0],\n",
+ " [3.0, 5.0, 7.0],\n",
+ " [0.0, 0.0, 0.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "a[3] = 10\n",
+ "print('a:\\n', a)\n",
+ "\n",
+ "print('\\nfirst derivative:\\n', np.diff(a, n=1))\n",
+ "print('\\nsecond derivative:\\n', np.diff(a, n=2))\n",
+ "\n",
+ "c = np.array([[1, 2, 3, 4], [4, 3, 2, 1], [1, 4, 9, 16], [0, 0, 0, 0]])\n",
+ "print('\\nc:\\n', c)\n",
+ "print('\\nfirst derivative, first axis:\\n', np.diff(c, axis=0))\n",
+ "print('\\nfirst derivative, second axis:\\n', np.diff(c, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## dot\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html\n",
+ "\n",
+ "\n",
+ "**WARNING:** numpy applies upcasting rules for the multiplication of matrices, while `ulab` simply returns a float matrix. \n",
+ "\n",
+ "Once you can invert a matrix, you might want to know, whether the inversion is correct. You can simply take the original matrix and its inverse, and multiply them by calling the `dot` function, which takes the two matrices as its arguments. If the matrix dimensions do not match, the function raises a `ValueError`. The result of the multiplication is expected to be the unit matrix, which is demonstrated below."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-13T08:32:09.139378Z",
+ "start_time": "2021-02-13T08:32:09.122083Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "m:\n",
+ " array([[1, 2, 3],\n",
+ " [4, 5, 6],\n",
+ " [7, 10, 9]], dtype=uint8)\n",
+ "\n",
+ "m^-1:\n",
+ " array([[-1.25, 1.0, -0.25],\n",
+ " [0.4999999999999998, -1.0, 0.5],\n",
+ " [0.4166666666666668, 0.3333333333333333, -0.25]], dtype=float64)\n",
+ "\n",
+ "m*m^-1:\n",
+ " array([[1.0, 0.0, 0.0],\n",
+ " [4.440892098500626e-16, 1.0, 0.0],\n",
+ " [8.881784197001252e-16, 0.0, 1.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "m = np.array([[1, 2, 3], [4, 5, 6], [7, 10, 9]], dtype=np.uint8)\n",
+ "n = np.linalg.inv(m)\n",
+ "print(\"m:\\n\", m)\n",
+ "print(\"\\nm^-1:\\n\", n)\n",
+ "# this should be the unit matrix\n",
+ "print(\"\\nm*m^-1:\\n\", np.dot(m, n))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that for matrix multiplication you don't necessarily need square matrices, it is enough, if their dimensions are compatible (i.e., the the left-hand-side matrix has as many columns, as does the right-hand-side matrix rows):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-13T08:33:07.630825Z",
+ "start_time": "2021-02-13T08:33:07.608260Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[1, 2, 3, 4],\n",
+ " [5, 6, 7, 8]], dtype=uint8)\n",
+ "array([[1, 2],\n",
+ " [3, 4],\n",
+ " [5, 6],\n",
+ " [7, 8]], dtype=uint8)\n",
+ "array([[50.0, 60.0],\n",
+ " [114.0, 140.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "m = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.uint8)\n",
+ "n = np.array([[1, 2], [3, 4], [5, 6], [7, 8]], dtype=np.uint8)\n",
+ "print(m)\n",
+ "print(n)\n",
+ "print(np.dot(m, n))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## equal\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.equal.html\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.not_equal.html\n",
+ "\n",
+ "In `micropython`, equality of arrays or scalars can be established by utilising the `==`, `!=`, `<`, `>`, `<=`, or `=>` binary operators. In `circuitpython`, `==` and `!=` will produce unexpected results. In order to avoid this discrepancy, and to maintain compatibility with `numpy`, `ulab` implements the `equal` and `not_equal` operators that return the same results, irrespective of the `python` implementation.\n",
+ "\n",
+ "These two functions take two `ndarray`s, or scalars as their arguments. No keyword arguments are implemented."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T14:22:13.990898Z",
+ "start_time": "2021-01-08T14:22:13.941896Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "b: array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=float64)\n",
+ "\n",
+ "a == b: array([True, False, False, False, False, False, False, False, False], dtype=bool)\n",
+ "a != b: array([False, True, True, True, True, True, True, True, True], dtype=bool)\n",
+ "a == 2: array([False, False, True, False, False, False, False, False, False], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9))\n",
+ "b = np.zeros(9)\n",
+ "\n",
+ "print('a: ', a)\n",
+ "print('b: ', b)\n",
+ "print('\\na == b: ', np.equal(a, b))\n",
+ "print('a != b: ', np.not_equal(a, b))\n",
+ "\n",
+ "# comparison with scalars\n",
+ "print('a == 2: ', np.equal(a, 2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## flip\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html\n",
+ "\n",
+ "The `flip` function takes one positional, an `ndarray`, and one keyword argument, `axis = None`, and reverses the order of elements along the given axis. If the keyword argument is `None`, the matrix' entries are flipped along all axes. `flip` returns a new copy of the array.\n",
+ "\n",
+ "If the firmware was compiled with complex support, the function can accept complex arrays."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:25:08.425583Z",
+ "start_time": "2021-01-13T16:25:08.407004Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \t array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=float64)\n",
+ "a flipped:\t array([5.0, 4.0, 3.0, 2.0, 1.0], dtype=float64)\n",
+ "\n",
+ "a flipped horizontally\n",
+ " array([[3, 2, 1],\n",
+ " [6, 5, 4],\n",
+ " [9, 8, 7]], dtype=uint8)\n",
+ "\n",
+ "a flipped vertically\n",
+ " array([[7, 8, 9],\n",
+ " [4, 5, 6],\n",
+ " [1, 2, 3]], dtype=uint8)\n",
+ "\n",
+ "a flipped horizontally+vertically\n",
+ " array([9, 8, 7, 6, 5, 4, 3, 2, 1], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5])\n",
+ "print(\"a: \\t\", a)\n",
+ "print(\"a flipped:\\t\", np.flip(a))\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)\n",
+ "print(\"\\na flipped horizontally\\n\", np.flip(a, axis=1))\n",
+ "print(\"\\na flipped vertically\\n\", np.flip(a, axis=0))\n",
+ "print(\"\\na flipped horizontally+vertically\\n\", np.flip(a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## imag\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.imag.html\n",
+ "\n",
+ "The `imag` function returns the imaginary part of an array, or scalar. It cannot accept a generic iterable as its argument. The function is defined only, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:26:42.901258Z",
+ "start_time": "2022-01-07T19:26:42.880755Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([1, 2, 3], dtype=uint16)\n",
+ "imag(a):\t array([0, 0, 0], dtype=uint16)\n",
+ "\n",
+ "b:\t\t array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)\n",
+ "imag(b):\t array([0.0, 1.0, -1.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3], dtype=np.uint16)\n",
+ "print(\"a:\\t\\t\", a)\n",
+ "print(\"imag(a):\\t\", np.imag(a))\n",
+ "\n",
+ "b = np.array([1, 2+1j, 3-1j], dtype=np.complex)\n",
+ "print(\"\\nb:\\t\\t\", b)\n",
+ "print(\"imag(b):\\t\", np.imag(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## interp\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/numpy.interp\n",
+ "\n",
+ "The `interp` function returns the linearly interpolated values of a one-dimensional numerical array. It requires three positional arguments,`x`, at which the interpolated values are evaluated, `xp`, the array\n",
+ "of the independent data variable, and `fp`, the array of the dependent values of the data. `xp` must be a monotonically increasing sequence of numbers.\n",
+ "\n",
+ "Two keyword arguments, `left`, and `right` can also be supplied; these determine the return values, if `x < xp[0]`, and `x > xp[-1]`, respectively. If these arguments are not supplied, `left`, and `right` default to `fp[0]`, and `fp[-1]`, respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:00:43.505722Z",
+ "start_time": "2021-01-13T16:00:43.489060Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([0.8, 1.8, 2.8, 3.8, 4.8], dtype=float64)\n",
+ "array([1.0, 1.8, 2.8, 4.6, 5.0], dtype=float64)\n",
+ "array([0.0, 1.8, 2.8, 4.6, 5.0], dtype=float64)\n",
+ "array([1.0, 1.8, 2.8, 4.6, 10.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.array([1, 2, 3, 4, 5]) - 0.2\n",
+ "xp = np.array([1, 2, 3, 4])\n",
+ "fp = np.array([1, 2, 3, 5])\n",
+ "\n",
+ "print(x)\n",
+ "print(np.interp(x, xp, fp))\n",
+ "print(np.interp(x, xp, fp, left=0.0))\n",
+ "print(np.interp(x, xp, fp, right=10.0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## isfinite\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.isfinite.html\n",
+ "\n",
+ "Returns a Boolean array of the same shape as the input, or a `True/False`, if the input is a scalar. In the return value, all elements are `True` at positions, where the input value was finite. Integer types are automatically finite, therefore, if the input is of integer type, the output will be the `True` tensor."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-29T21:34:42.026689Z",
+ "start_time": "2021-01-29T21:34:42.010935Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "isfinite(0): True\n",
+ "\n",
+ "====================\n",
+ "a:\n",
+ " array([1.0, 2.0, nan], dtype=float64)\n",
+ "\n",
+ "isfinite(a):\n",
+ " array([True, True, False], dtype=bool)\n",
+ "\n",
+ "====================\n",
+ "b:\n",
+ " array([1.0, 2.0, inf], dtype=float64)\n",
+ "\n",
+ "isfinite(b):\n",
+ " array([True, True, False], dtype=bool)\n",
+ "\n",
+ "====================\n",
+ "c:\n",
+ " array([1, 2, 3], dtype=uint16)\n",
+ "\n",
+ "isfinite(c):\n",
+ " array([True, True, True], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print('isfinite(0): ', np.isfinite(0))\n",
+ "\n",
+ "a = np.array([1, 2, np.nan])\n",
+ "print('\\n' + '='*20)\n",
+ "print('a:\\n', a)\n",
+ "print('\\nisfinite(a):\\n', np.isfinite(a))\n",
+ "\n",
+ "b = np.array([1, 2, np.inf])\n",
+ "print('\\n' + '='*20)\n",
+ "print('b:\\n', b)\n",
+ "print('\\nisfinite(b):\\n', np.isfinite(b))\n",
+ "\n",
+ "c = np.array([1, 2, 3], dtype=np.uint16)\n",
+ "print('\\n' + '='*20)\n",
+ "print('c:\\n', c)\n",
+ "print('\\nisfinite(c):\\n', np.isfinite(c))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## isinf\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.isinf.html\n",
+ "\n",
+ "Similar to [isfinite](#isfinite), but the output is `True` at positions, where the input is infinite. Integer types return the `False` tensor."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-29T21:35:21.938514Z",
+ "start_time": "2021-01-29T21:35:21.923741Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "isinf(0): False\n",
+ "\n",
+ "====================\n",
+ "a:\n",
+ " array([1.0, 2.0, nan], dtype=float64)\n",
+ "\n",
+ "isinf(a):\n",
+ " array([False, False, False], dtype=bool)\n",
+ "\n",
+ "====================\n",
+ "b:\n",
+ " array([1.0, 2.0, inf], dtype=float64)\n",
+ "\n",
+ "isinf(b):\n",
+ " array([False, False, True], dtype=bool)\n",
+ "\n",
+ "====================\n",
+ "c:\n",
+ " array([1, 2, 3], dtype=uint16)\n",
+ "\n",
+ "isinf(c):\n",
+ " array([False, False, False], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print('isinf(0): ', np.isinf(0))\n",
+ "\n",
+ "a = np.array([1, 2, np.nan])\n",
+ "print('\\n' + '='*20)\n",
+ "print('a:\\n', a)\n",
+ "print('\\nisinf(a):\\n', np.isinf(a))\n",
+ "\n",
+ "b = np.array([1, 2, np.inf])\n",
+ "print('\\n' + '='*20)\n",
+ "print('b:\\n', b)\n",
+ "print('\\nisinf(b):\\n', np.isinf(b))\n",
+ "\n",
+ "c = np.array([1, 2, 3], dtype=np.uint16)\n",
+ "print('\\n' + '='*20)\n",
+ "print('c:\\n', c)\n",
+ "print('\\nisinf(c):\\n', np.isinf(c))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## mean\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.mean.html\n",
+ "\n",
+ "If the axis keyword is not specified, it assumes the default value of `None`, and returns the result of the computation for the flattened array. Otherwise, the calculation is along the given axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:15:39.921212Z",
+ "start_time": "2021-01-13T16:15:39.908217Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \n",
+ " array([[1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0],\n",
+ " [7.0, 8.0, 9.0]], dtype=float64)\n",
+ "mean, flat: 5.0\n",
+ "mean, horizontal: array([2.0, 5.0, 8.0], dtype=float64)\n",
+ "mean, vertical: array([4.0, 5.0, 6.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
+ "print('a: \\n', a)\n",
+ "print('mean, flat: ', np.mean(a))\n",
+ "print('mean, horizontal: ', np.mean(a, axis=1))\n",
+ "print('mean, vertical: ', np.mean(a, axis=0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## max\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.max.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.min.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmin.html\n",
+ "\n",
+ "**WARNING:** Difference to `numpy`: the `out` keyword argument is not implemented.\n",
+ "\n",
+ "These functions follow the same pattern, and work with generic iterables, and `ndarray`s. `min`, and `max` return the minimum or maximum of a sequence. If the input array is two-dimensional, the `axis` keyword argument can be supplied, in which case the minimum/maximum along the given axis will be returned. If `axis=None` (this is also the default value), the minimum/maximum of the flattened array will be determined.\n",
+ "\n",
+ "`argmin/argmax` return the position (index) of the minimum/maximum in the sequence."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:08:56.986619Z",
+ "start_time": "2021-01-13T16:08:56.964492Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([1.0, 2.0, 0.0, 1.0, 10.0], dtype=float64)\n",
+ "min of a: 0.0\n",
+ "argmin of a: 2\n",
+ "\n",
+ "b:\n",
+ " array([[1.0, 2.0, 0.0],\n",
+ " [1.0, 10.0, -1.0]], dtype=float64)\n",
+ "min of b (flattened): -1.0\n",
+ "min of b (axis=0): array([1.0, 2.0, -1.0], dtype=float64)\n",
+ "min of b (axis=1): array([0.0, -1.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 0, 1, 10])\n",
+ "print('a:', a)\n",
+ "print('min of a:', np.min(a))\n",
+ "print('argmin of a:', np.argmin(a))\n",
+ "\n",
+ "b = np.array([[1, 2, 0], [1, 10, -1]])\n",
+ "print('\\nb:\\n', b)\n",
+ "print('min of b (flattened):', np.min(b))\n",
+ "print('min of b (axis=0):', np.min(b, axis=0))\n",
+ "print('min of b (axis=1):', np.min(b, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## median\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.median.html\n",
+ "\n",
+ "The function computes the median along the specified axis, and returns the median of the array elements. If the `axis` keyword argument is `None`, the arrays is flattened first. The `dtype` of the results is always float."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 34,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:31:13.833800Z",
+ "start_time": "2021-01-13T16:31:13.809560Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[0, 1, 2, 3],\n",
+ " [4, 5, 6, 7],\n",
+ " [8, 9, 10, 11]], dtype=int8)\n",
+ "\n",
+ "median of the flattened array: 5.5\n",
+ "\n",
+ "median along the vertical axis: array([4.0, 5.0, 6.0, 7.0], dtype=float64)\n",
+ "\n",
+ "median along the horizontal axis: array([1.5, 5.5, 9.5], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(12), dtype=np.int8).reshape((3, 4))\n",
+ "print('a:\\n', a)\n",
+ "print('\\nmedian of the flattened array: ', np.median(a))\n",
+ "print('\\nmedian along the vertical axis: ', np.median(a, axis=0))\n",
+ "print('\\nmedian along the horizontal axis: ', np.median(a, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## min\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.min.html\n",
+ "\n",
+ "See [numpy.max](#max)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## minimum\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.minimum.html\n",
+ "\n",
+ "See [numpy.maximum](#maximum)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## maximum\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.maximum.html\n",
+ "\n",
+ "Returns the maximum of two arrays, or two scalars, or an array, and a scalar. If the arrays are of different `dtype`, the output is upcast as in [Binary operators](#Binary-operators). If both inputs are scalars, a scalar is returned. Only positional arguments are implemented."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:21:17.151280Z",
+ "start_time": "2021-01-08T13:21:17.123768Z"
+ },
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "minimum of a, and b:\n",
+ "array([1.0, 2.0, 3.0, 2.0, 1.0], dtype=float64)\n",
+ "\n",
+ "maximum of a, and b:\n",
+ "array([5.0, 4.0, 3.0, 4.0, 5.0], dtype=float64)\n",
+ "\n",
+ "maximum of 1, and 5.5:\n",
+ "5.5\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)\n",
+ "b = np.array([5, 4, 3, 2, 1], dtype=np.float)\n",
+ "print('minimum of a, and b:')\n",
+ "print(np.minimum(a, b))\n",
+ "\n",
+ "print('\\nmaximum of a, and b:')\n",
+ "print(np.maximum(a, b))\n",
+ "\n",
+ "print('\\nmaximum of 1, and 5.5:')\n",
+ "print(np.maximum(1, 5.5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## not_equal\n",
+ "\n",
+ "See [numpy.equal](#equal)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## polyfit\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html\n",
+ "\n",
+ "polyfit takes two, or three arguments. The last one is the degree of the polynomial that will be fitted, the last but one is an array or iterable with the `y` (dependent) values, and the first one, an array or iterable with the `x` (independent) values, can be dropped. If that is the case, `x` will be generated in the function as `range(len(y))`.\n",
+ "\n",
+ "If the lengths of `x`, and `y` are not the same, the function raises a `ValueError`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:23:39.238450Z",
+ "start_time": "2021-01-13T18:23:39.221063Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "independent values:\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=float64)\n",
+ "dependent values:\t array([9.0, 4.0, 1.0, 0.0, 1.0, 4.0, 9.0], dtype=float64)\n",
+ "fitted values:\t\t array([1.0, -6.0, 9.000000000000004], dtype=float64)\n",
+ "\n",
+ "dependent values:\t array([9.0, 4.0, 1.0, 0.0, 1.0, 4.0, 9.0], dtype=float64)\n",
+ "fitted values:\t\t array([1.0, -6.0, 9.000000000000004], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.array([0, 1, 2, 3, 4, 5, 6])\n",
+ "y = np.array([9, 4, 1, 0, 1, 4, 9])\n",
+ "print('independent values:\\t', x)\n",
+ "print('dependent values:\\t', y)\n",
+ "print('fitted values:\\t\\t', np.polyfit(x, y, 2))\n",
+ "\n",
+ "# the same with missing x\n",
+ "print('\\ndependent values:\\t', y)\n",
+ "print('fitted values:\\t\\t', np.polyfit(y, 2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Execution time\n",
+ "\n",
+ "`polyfit` is based on the inversion of a matrix (there is more on the background in https://en.wikipedia.org/wiki/Polynomial_regression), and it requires the intermediate storage of `2*N*(deg+1)` floats, where `N` is the number of entries in the input array, and `deg` is the fit's degree. The additional computation costs of the matrix inversion discussed in [linalg.inv](#inv) also apply. The example from above needs around 150 microseconds to return:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:31:40.919764Z",
+ "start_time": "2021-01-13T18:31:40.912817Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "execution time: 153 us\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "@timeit\n",
+ "def time_polyfit(x, y, n):\n",
+ " return np.polyfit(x, y, n)\n",
+ "\n",
+ "x = np.array([0, 1, 2, 3, 4, 5, 6])\n",
+ "y = np.array([9, 4, 1, 0, 1, 4, 9])\n",
+ "\n",
+ "time_polyfit(x, y, 2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## polyval\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyval.html\n",
+ "\n",
+ "`polyval` takes two arguments, both arrays or generic `micropython` iterables returning scalars."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:12:56.736643Z",
+ "start_time": "2021-01-13T18:12:56.668042Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "coefficients: [1, 1, 1, 0]\n",
+ "independent values: [0, 1, 2, 3, 4]\n",
+ "\n",
+ "values of p(x): array([0.0, 3.0, 14.0, 39.0, 84.0], dtype=float64)\n",
+ "\n",
+ "ndarray (a): array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float64)\n",
+ "value of p(a): array([0.0, 3.0, 14.0, 39.0, 84.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "p = [1, 1, 1, 0]\n",
+ "x = [0, 1, 2, 3, 4]\n",
+ "print('coefficients: ', p)\n",
+ "print('independent values: ', x)\n",
+ "print('\\nvalues of p(x): ', np.polyval(p, x))\n",
+ "\n",
+ "# the same works with one-dimensional ndarrays\n",
+ "a = np.array(x)\n",
+ "print('\\nndarray (a): ', a)\n",
+ "print('value of p(a): ', np.polyval(p, a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## real\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.real.html\n",
+ "\n",
+ "The `real` function returns the real part of an array, or scalar. It cannot accept a generic iterable as its argument. The function is defined only, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:27:22.141930Z",
+ "start_time": "2022-01-07T19:27:22.122577Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([1, 2, 3], dtype=uint16)\n",
+ "real(a):\t array([1, 2, 3], dtype=uint16)\n",
+ "\n",
+ "b:\t\t array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)\n",
+ "real(b):\t array([1.0, 2.0, 3.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3], dtype=np.uint16)\n",
+ "print(\"a:\\t\\t\", a)\n",
+ "print(\"real(a):\\t\", np.real(a))\n",
+ "\n",
+ "b = np.array([1, 2+1j, 3-1j], dtype=np.complex)\n",
+ "print(\"\\nb:\\t\\t\", b)\n",
+ "print(\"real(b):\\t\", np.real(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## roll\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.roll.html\n",
+ "\n",
+ "The roll function shifts the content of a vector by the positions given as the second argument. If the `axis` keyword is supplied, the shift is applied to the given axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:18:30.387043Z",
+ "start_time": "2021-01-13T16:18:30.363374Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "a rolled to the left:\t array([7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=float64)\n",
+ "a rolled to the right:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5, 6, 7, 8])\n",
+ "print(\"a:\\t\\t\\t\", a)\n",
+ "\n",
+ "a = np.roll(a, 2)\n",
+ "print(\"a rolled to the left:\\t\", a)\n",
+ "\n",
+ "# this should be the original vector\n",
+ "a = np.roll(a, -2)\n",
+ "print(\"a rolled to the right:\\t\", a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Rolling works with matrices, too. If the `axis` keyword is 0, the matrix is rolled along its vertical axis, otherwise, horizontally. \n",
+ "\n",
+ "Horizontal rolls are faster, because they require fewer steps, and larger memory chunks are copied, however, they also require more RAM: basically the whole row must be stored internally. Most expensive are the `None` keyword values, because with `axis = None`, the array is flattened first, hence the row's length is the size of the whole matrix.\n",
+ "\n",
+ "Vertical rolls require two internal copies of single columns. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:23:52.025977Z",
+ "start_time": "2021-01-13T16:23:52.001252Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[0.0, 1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0]], dtype=float64)\n",
+ "\n",
+ "a rolled up:\n",
+ " array([[4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0],\n",
+ " [0.0, 1.0, 2.0, 3.0]], dtype=float64)\n",
+ "a:\n",
+ " array([[0.0, 1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0]], dtype=float64)\n",
+ "\n",
+ "a rolled to the left:\n",
+ " array([[1.0, 2.0, 3.0, 0.0],\n",
+ " [5.0, 6.0, 7.0, 4.0],\n",
+ " [9.0, 10.0, 11.0, 8.0]], dtype=float64)\n",
+ "a:\n",
+ " array([[0.0, 1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0]], dtype=float64)\n",
+ "\n",
+ "a rolled with None:\n",
+ " array([[11.0, 0.0, 1.0, 2.0],\n",
+ " [3.0, 4.0, 5.0, 6.0],\n",
+ " [7.0, 8.0, 9.0, 10.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(12)).reshape((3, 4))\n",
+ "print(\"a:\\n\", a)\n",
+ "a = np.roll(a, 2, axis=0)\n",
+ "print(\"\\na rolled up:\\n\", a)\n",
+ "\n",
+ "a = np.array(range(12)).reshape((3, 4))\n",
+ "print(\"a:\\n\", a)\n",
+ "a = np.roll(a, -1, axis=1)\n",
+ "print(\"\\na rolled to the left:\\n\", a)\n",
+ "\n",
+ "a = np.array(range(12)).reshape((3, 4))\n",
+ "print(\"a:\\n\", a)\n",
+ "a = np.roll(a, 1, axis=None)\n",
+ "print(\"\\na rolled with None:\\n\", a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sort\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html\n",
+ "\n",
+ "The sort function takes an ndarray, and sorts its elements in ascending order along the specified axis using a heap sort algorithm. As opposed to the `.sort()` method discussed earlier, this function creates a copy of its input before sorting, and at the end, returns this copy. Sorting takes place in place, without auxiliary storage. The `axis` keyword argument takes on the possible values of -1 (the last axis, in `ulab` equivalent to the second axis, and this also happens to be the default value), 0, 1, or `None`. The first three cases are identical to those in [diff](#diff), while the last one flattens the array before sorting. \n",
+ "\n",
+ "If descending order is required, the result can simply be `flip`ped, see [flip](#flip).\n",
+ "\n",
+ "**WARNING:** `numpy` defines the `kind`, and `order` keyword arguments that are not implemented here. The function in `ulab` always uses heap sort, and since `ulab` does not have the concept of data fields, the `order` keyword argument would have no meaning."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:32:07.748972Z",
+ "start_time": "2021-01-13T16:32:07.730498Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[1.0, 12.0, 3.0, 0.0],\n",
+ " [5.0, 3.0, 4.0, 1.0],\n",
+ " [9.0, 11.0, 1.0, 8.0],\n",
+ " [7.0, 10.0, 0.0, 1.0]], dtype=float64)\n",
+ "\n",
+ "a sorted along vertical axis:\n",
+ " array([[1.0, 3.0, 0.0, 0.0],\n",
+ " [5.0, 10.0, 1.0, 1.0],\n",
+ " [7.0, 11.0, 3.0, 1.0],\n",
+ " [9.0, 12.0, 4.0, 8.0]], dtype=float64)\n",
+ "\n",
+ "a sorted along horizontal axis:\n",
+ " array([[0.0, 1.0, 3.0, 12.0],\n",
+ " [1.0, 3.0, 4.0, 5.0],\n",
+ " [1.0, 8.0, 9.0, 11.0],\n",
+ " [0.0, 1.0, 7.0, 10.0]], dtype=float64)\n",
+ "\n",
+ "flattened a sorted:\n",
+ " array([0.0, 0.0, 1.0, ..., 10.0, 11.0, 12.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)\n",
+ "print('\\na:\\n', a)\n",
+ "b = np.sort(a, axis=0)\n",
+ "print('\\na sorted along vertical axis:\\n', b)\n",
+ "\n",
+ "c = np.sort(a, axis=1)\n",
+ "print('\\na sorted along horizontal axis:\\n', c)\n",
+ "\n",
+ "c = np.sort(a, axis=None)\n",
+ "print('\\nflattened a sorted:\\n', c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Heap sort requires $\\sim N\\log N$ operations, and notably, the worst case costs only 20% more time than the average. In order to get an order-of-magnitude estimate, we will take the sine of 1000 uniformly spaced numbers between 0, and two pi, and sort them:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "@timeit\n",
+ "def sort_time(array):\n",
+ " return nup.sort(array)\n",
+ "\n",
+ "b = np.sin(np.linspace(0, 6.28, num=1000))\n",
+ "print('b: ', b)\n",
+ "sort_time(b)\n",
+ "print('\\nb sorted:\\n', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sort_complex\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.sort_complex.html\n",
+ "\n",
+ "If the firmware was compiled with complex support, the functions sorts the input array first according to its real part, and then the imaginary part. The input must be a one-dimensional array. The output is always of `dtype` complex, even if the input was real integer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:36:15.750029Z",
+ "start_time": "2022-01-07T19:36:15.732210Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t\t array([5, 4, 3, 2, 1], dtype=int16)\n",
+ "sort_complex(a):\t array([1.0+0.0j, 2.0+0.0j, 3.0+0.0j, 4.0+0.0j, 5.0+0.0j], dtype=complex)\n",
+ "\n",
+ "b:\t\t\t array([5.0+0.0j, 4.0+3.0j, 4.0-2.0j, 0.0+0.0j, 0.0+1.0j], dtype=complex)\n",
+ "sort_complex(b):\t array([0.0+0.0j, 0.0+1.0j, 4.0-2.0j, 4.0+3.0j, 5.0+0.0j], dtype=complex)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([5, 4, 3, 2, 1], dtype=np.int16)\n",
+ "print('a:\\t\\t\\t', a)\n",
+ "print('sort_complex(a):\\t', np.sort_complex(a))\n",
+ "print()\n",
+ "\n",
+ "b = np.array([5, 4+3j, 4-2j, 0, 1j], dtype=np.complex)\n",
+ "print('b:\\t\\t\\t', b)\n",
+ "print('sort_complex(b):\\t', np.sort_complex(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## std\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.std.html\n",
+ "\n",
+ "If the axis keyword is not specified, it assumes the default value of `None`, and returns the result of the computation for the flattened array. Otherwise, the calculation is along the given axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:14:54.051061Z",
+ "start_time": "2021-01-13T16:14:54.029924Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \n",
+ " array([[1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0],\n",
+ " [7.0, 8.0, 9.0]], dtype=float64)\n",
+ "sum, flat array: 2.581988897471611\n",
+ "std, vertical: array([2.449489742783178, 2.449489742783178, 2.449489742783178], dtype=float64)\n",
+ "std, horizonal: array([0.8164965809277261, 0.8164965809277261, 0.8164965809277261], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
+ "print('a: \\n', a)\n",
+ "print('sum, flat array: ', np.std(a))\n",
+ "print('std, vertical: ', np.std(a, axis=0))\n",
+ "print('std, horizonal: ', np.std(a, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sum\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sum.html\n",
+ "\n",
+ "If the axis keyword is not specified, it assumes the default value of `None`, and returns the result of the computation for the flattened array. Otherwise, the calculation is along the given axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:14:34.576723Z",
+ "start_time": "2021-01-13T16:14:34.556304Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \n",
+ " array([[1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0],\n",
+ " [7.0, 8.0, 9.0]], dtype=float64)\n",
+ "sum, flat array: 45.0\n",
+ "sum, horizontal: array([6.0, 15.0, 24.0], dtype=float64)\n",
+ "std, vertical: array([12.0, 15.0, 18.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
+ "print('a: \\n', a)\n",
+ "\n",
+ "print('sum, flat array: ', np.sum(a))\n",
+ "print('sum, horizontal: ', np.sum(a, axis=1))\n",
+ "print('std, vertical: ', np.sum(a, axis=0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## trace\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.trace.html\n",
+ "\n",
+ "The `trace` function returns the sum of the diagonal elements of a square matrix. If the input argument is not a square matrix, an exception will be raised.\n",
+ "\n",
+ "The scalar so returned will inherit the type of the input array, i.e., integer arrays have integer trace, and floating point arrays a floating point trace."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-13T08:30:25.211965Z",
+ "start_time": "2021-02-13T08:30:25.195102Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([[25, 15, -5],\n",
+ " [15, 18, 0],\n",
+ " [-5, 0, 11]], dtype=int8)\n",
+ "\n",
+ "trace of a: 54\n",
+ "====================\n",
+ "b: array([[25.0, 15.0, -5.0],\n",
+ " [15.0, 18.0, 0.0],\n",
+ " [-5.0, 0.0, 11.0]], dtype=float64)\n",
+ "\n",
+ "trace of b: 54.0\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[25, 15, -5], [15, 18, 0], [-5, 0, 11]], dtype=np.int8)\n",
+ "print('a: ', a)\n",
+ "print('\\ntrace of a: ', np.trace(a))\n",
+ "\n",
+ "b = np.array([[25, 15, -5], [15, 18, 0], [-5, 0, 11]], dtype=np.float)\n",
+ "\n",
+ "print('='*20 + '\\nb: ', b)\n",
+ "print('\\ntrace of b: ', np.trace(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## trapz\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.trapz.html\n",
+ "\n",
+ "The function takes one or two one-dimensional `ndarray`s, and integrates the dependent values (`y`) using the trapezoidal rule. If the independent variable (`x`) is given, that is taken as the sample points corresponding to `y`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T16:03:42.566302Z",
+ "start_time": "2021-01-13T16:03:42.545630Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "x: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], dtype=float64)\n",
+ "y: array([0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0], dtype=float64)\n",
+ "============================\n",
+ "integral of y: 244.5\n",
+ "integral of y at x: 244.5\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.linspace(0, 9, num=10)\n",
+ "y = x*x\n",
+ "\n",
+ "print('x: ', x)\n",
+ "print('y: ', y)\n",
+ "print('============================')\n",
+ "print('integral of y: ', np.trapz(y))\n",
+ "print('integral of y at x: ', np.trapz(y, x=x))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## where\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.where.html\n",
+ "\n",
+ "The function takes three positional arguments, `condition`, `x`, and `y`, and returns a new `ndarray`, whose values are taken from either `x`, or `y`, depending on the truthness of `condition`. The three arguments are broadcast together, and the function raises a `ValueError` exception, if broadcasting is not possible.\n",
+ "\n",
+ "The function is implemented for `ndarray`s only: other iterable types can be passed after casting them to an `ndarray` by calling the `array` constructor.\n",
+ "\n",
+ "If the `dtype`s of `x`, and `y` differ, the output is upcast as discussed earlier. \n",
+ "\n",
+ "Note that the `condition` is expanded into an Boolean `ndarray`. This means that the storage required to hold the condition should be taken into account, whenever the function is called."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The following example returns an `ndarray` of length 4, with 1 at positions, where `condition` is smaller than 3, and with -1 otherwise."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-23T16:18:14.396840Z",
+ "start_time": "2021-03-23T16:18:14.385134Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([1, 1, -1, -1], dtype=int16)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "condition = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
+ "print(np.where(condition < 3, 1, -1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The next snippet shows, how values from two arrays can be fed into the output:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-23T16:15:29.954224Z",
+ "start_time": "2021-03-23T16:15:29.937205Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([11, 22, 3, 4], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "condition = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
+ "x = np.array([11, 22, 33, 44], dtype=np.uint8)\n",
+ "y = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
+ "print(np.where(condition < 3, x, y))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/numpy-linalg.ipynb b/circuitpython/extmod/ulab/docs/numpy-linalg.ipynb
new file mode 100644
index 0000000..e57e63a
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/numpy-linalg.ipynb
@@ -0,0 +1,811 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "source": [
+ "%pylab inline"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:16:40.844266Z",
+ "start_time": "2021-01-13T06:16:39.992092Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Notebook magic"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ],
+ "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:16:40.857076Z",
+ "start_time": "2021-01-13T06:16:40.852721Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ],
+ "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:16:40.947944Z",
+ "start_time": "2021-01-13T06:16:40.865720Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## pyboard"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ],
+ "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ],
+ "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "__END_OF_DEFS__"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# numpy.linalg\n",
+ "\n",
+ "Functions in the `linalg` module can be called by prepending them by `numpy.linalg.`. The module defines the following seven functions:\n",
+ "\n",
+ "1. [numpy.linalg.cholesky](#cholesky)\n",
+ "1. [numpy.linalg.det](#det)\n",
+ "1. [numpy.linalg.eig](#eig)\n",
+ "1. [numpy.linalg.inv](#inv)\n",
+ "1. [numpy.linalg.norm](#norm)\n",
+ "1. [numpy.linalg.qr](#qr)"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## cholesky\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.linalg.cholesky.html\n",
+ "\n",
+ "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."
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[25, 15, -5], [15, 18, 0], [-5, 0, 11]])\n",
+ "print('a: ', a)\n",
+ "print('\\n' + '='*20 + '\\nCholesky decomposition\\n', np.linalg.cholesky(a))"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "a: array([[25.0, 15.0, -5.0],\n",
+ "\t [15.0, 18.0, 0.0],\n",
+ "\t [-5.0, 0.0, 11.0]], dtype=float)\n",
+ "\n",
+ "====================\n",
+ "Cholesky decomposition\n",
+ " array([[5.0, 0.0, 0.0],\n",
+ "\t [3.0, 3.0, 0.0],\n",
+ "\t [-1.0, 1.0, 3.0]], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-03-10T19:25:21.754166Z",
+ "start_time": "2020-03-10T19:25:21.740726Z"
+ },
+ "scrolled": true
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## det\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.det.html\n",
+ "\n",
+ "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."
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 495,
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2], [3, 4]], dtype=np.uint8)\n",
+ "print(np.linalg.det(a))"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "-2.0\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-19T13:27:24.246995Z",
+ "start_time": "2019-10-19T13:27:24.228698Z"
+ },
+ "scrolled": true
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Benchmark\n",
+ "\n",
+ "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:"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 557,
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "@timeit\n",
+ "def matrix_det(m):\n",
+ " return np.linalg.inv(m)\n",
+ "\n",
+ "m = np.array([[1, 2, 3, 4, 5, 6, 7, 8], [0, 5, 6, 4, 5, 6, 4, 5], \n",
+ " [0, 0, 9, 7, 8, 9, 7, 8], [0, 0, 0, 10, 11, 12, 11, 12], \n",
+ " [0, 0, 0, 0, 4, 6, 7, 8], [0, 0, 0, 0, 0, 5, 6, 7], \n",
+ " [0, 0, 0, 0, 0, 0, 7, 6], [0, 0, 0, 0, 0, 0, 0, 2]])\n",
+ "\n",
+ "matrix_det(m)"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "execution time: 294 us\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-20T07:14:59.778987Z",
+ "start_time": "2019-10-20T07:14:59.740021Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## eig\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.eig.html\n",
+ "\n",
+ "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."
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 1, 4], [2, 5, 3, 5], [1, 3, 6, 1], [4, 5, 1, 7]], dtype=np.uint8)\n",
+ "x, y = np.linalg.eig(a)\n",
+ "print('eigenvectors of a:\\n', y)\n",
+ "print('\\neigenvalues of a:\\n', x)"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "eigenvectors of a:\n",
+ " array([[0.8151560042509081, -0.4499411232970823, -0.1644660242574522, 0.3256141906686505],\n",
+ " [0.2211334179893007, 0.7846992598235538, 0.08372081379922657, 0.5730077734355189],\n",
+ " [-0.1340114162071679, -0.3100776411558949, 0.8742786816656, 0.3486109343758527],\n",
+ " [-0.5183258053659028, -0.292663481927148, -0.4489749870391468, 0.6664142156731531]], dtype=float)\n",
+ "\n",
+ "eigenvalues of a:\n",
+ " array([-1.165288365404889, 0.8029365530314914, 5.585625756072663, 13.77672605630074], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-11-03T20:25:26.952290Z",
+ "start_time": "2020-11-03T20:25:26.930184Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The same matrix diagonalised with `numpy` yields:"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "source": [
+ "a = array([[1, 2, 1, 4], [2, 5, 3, 5], [1, 3, 6, 1], [4, 5, 1, 7]], dtype=np.uint8)\n",
+ "x, y = eig(a)\n",
+ "print('eigenvectors of a:\\n', y)\n",
+ "print('\\neigenvalues of a:\\n', x)"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "eigenvectors of a:\n",
+ " [[ 0.32561419 0.815156 0.44994112 -0.16446602]\n",
+ " [ 0.57300777 0.22113342 -0.78469926 0.08372081]\n",
+ " [ 0.34861093 -0.13401142 0.31007764 0.87427868]\n",
+ " [ 0.66641421 -0.51832581 0.29266348 -0.44897499]]\n",
+ "\n",
+ "eigenvalues of a:\n",
+ " [13.77672606 -1.16528837 0.80293655 5.58562576]\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-11-03T20:13:27.236159Z",
+ "start_time": "2020-11-03T20:13:27.069967Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "When comparing results, we should keep two things in mind: \n",
+ "\n",
+ "1. the eigenvalues and eigenvectors are not necessarily sorted in the same way\n",
+ "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. "
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Computation expenses\n",
+ "\n",
+ "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:"
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 559,
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "@timeit\n",
+ "def matrix_eig(a):\n",
+ " return np.linalg.eig(a)\n",
+ "\n",
+ "a = np.array([[1, 2, 1, 4], [2, 5, 3, 5], [1, 3, 6, 1], [4, 5, 1, 7]], dtype=np.uint8)\n",
+ "\n",
+ "matrix_eig(a)"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "execution time: 111 us\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-20T07:18:52.520515Z",
+ "start_time": "2019-10-20T07:18:52.499653Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## inv\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.linalg.inv.html\n",
+ "\n",
+ "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)."
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "m = np.array([[1, 2, 3, 4], [4, 5, 6, 4], [7, 8.6, 9, 4], [3, 4, 5, 6]])\n",
+ "\n",
+ "print(np.linalg.inv(m))"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "array([[-2.166666666666667, 1.500000000000001, -0.8333333333333337, 1.0],\n",
+ " [1.666666666666667, -3.333333333333335, 1.666666666666668, -0.0],\n",
+ " [0.1666666666666666, 2.166666666666668, -0.8333333333333337, -1.0],\n",
+ " [-0.1666666666666667, -0.3333333333333333, 0.0, 0.5]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:17:13.053816Z",
+ "start_time": "2021-01-13T06:17:13.038403Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Computation expenses\n",
+ "\n",
+ "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: "
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 552,
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "@timeit\n",
+ "def invert_matrix(m):\n",
+ " return np.linalg.inv(m)\n",
+ "\n",
+ "m = np.array([[1, 2,], [4, 5]])\n",
+ "print('2 by 2 matrix:')\n",
+ "invert_matrix(m)\n",
+ "\n",
+ "m = np.array([[1, 2, 3, 4], [4, 5, 6, 4], [7, 8.6, 9, 4], [3, 4, 5, 6]])\n",
+ "print('\\n4 by 4 matrix:')\n",
+ "invert_matrix(m)\n",
+ "\n",
+ "m = np.array([[1, 2, 3, 4, 5, 6, 7, 8], [0, 5, 6, 4, 5, 6, 4, 5], \n",
+ " [0, 0, 9, 7, 8, 9, 7, 8], [0, 0, 0, 10, 11, 12, 11, 12], \n",
+ " [0, 0, 0, 0, 4, 6, 7, 8], [0, 0, 0, 0, 0, 5, 6, 7], \n",
+ " [0, 0, 0, 0, 0, 0, 7, 6], [0, 0, 0, 0, 0, 0, 0, 2]])\n",
+ "print('\\n8 by 8 matrix:')\n",
+ "invert_matrix(m)"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "2 by 2 matrix:\n",
+ "execution time: 65 us\n",
+ "\n",
+ "4 by 4 matrix:\n",
+ "execution time: 105 us\n",
+ "\n",
+ "8 by 8 matrix:\n",
+ "execution time: 299 us\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-20T07:10:39.190734Z",
+ "start_time": "2019-10-20T07:10:39.138872Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "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. "
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## norm\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html\n",
+ "\n",
+ "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."
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5])\n",
+ "b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
+ "\n",
+ "print('norm of a:', np.linalg.norm(a))\n",
+ "print('norm of b:', np.linalg.norm(b))"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "norm of a: 7.416198487095663\n",
+ "norm of b: 16.88194301613414\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-07-23T20:41:10.341349Z",
+ "start_time": "2020-07-23T20:41:10.327624Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## qr\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.linalg.qr.html\n",
+ "\n",
+ "\n",
+ "The function computes the QR decomposition of a matrix `m` of dimensions `(M, N)`, i.e., it returns two such matrices, `q`', and `r`, that `m = qr`, where `q` is orthonormal, and `r` is upper triangular. In addition to the input matrix, which is the first positional argument, the function accepts the `mode` keyword argument with a default value of `reduced`. If `mode` is `reduced`, `q`, and `r` are returned in the reduced representation. Otherwise, the outputs will have dimensions `(M, M)`, and `(M, N)`, respectively."
+ ],
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "A = np.arange(6).reshape((3, 2))\n",
+ "print('A: \\n', A)\n",
+ "\n",
+ "print('complete decomposition')\n",
+ "q, r = np.linalg.qr(A, mode='complete')\n",
+ "print('q: \\n', q)\n",
+ "print()\n",
+ "print('r: \\n', r)\n",
+ "\n",
+ "print('\\n\\nreduced decomposition')\n",
+ "q, r = np.linalg.qr(A, mode='reduced')\n",
+ "print('q: \\n', q)\n",
+ "print()\n",
+ "print('r: \\n', r)\n"
+ ],
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "A: \n",
+ " array([[0, 1],\n",
+ " [2, 3],\n",
+ " [4, 5]], dtype=int16)\n",
+ "complete decomposition\n",
+ "q: \n",
+ " array([[0.0, -0.9128709291752768, 0.408248290463863],\n",
+ " [-0.447213595499958, -0.3651483716701107, -0.8164965809277261],\n",
+ " [-0.8944271909999159, 0.1825741858350553, 0.408248290463863]], dtype=float64)\n",
+ "\n",
+ "r: \n",
+ " array([[-4.47213595499958, -5.813776741499454],\n",
+ " [0.0, -1.095445115010332],\n",
+ " [0.0, 0.0]], dtype=float64)\n",
+ "\n",
+ "\n",
+ "reduced decomposition\n",
+ "q: \n",
+ " array([[0.0, -0.9128709291752768],\n",
+ " [-0.447213595499958, -0.3651483716701107],\n",
+ " [-0.8944271909999159, 0.1825741858350553]], dtype=float64)\n",
+ "\n",
+ "r: \n",
+ " array([[-4.47213595499958, -5.813776741499454],\n",
+ " [0.0, -1.095445115010332]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "metadata": {}
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3.8.5 64-bit ('base': conda)"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ },
+ "interpreter": {
+ "hash": "ce9a02f9f7db620716422019cafa4bc1786ca85daa298b819f6da075e7993842"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+} \ No newline at end of file
diff --git a/circuitpython/extmod/ulab/docs/numpy-universal.ipynb b/circuitpython/extmod/ulab/docs/numpy-universal.ipynb
new file mode 100644
index 0000000..8934fa6
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/numpy-universal.ipynb
@@ -0,0 +1,869 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:54:58.722373Z",
+ "start_time": "2021-01-13T18:54:57.178438Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:10:30.696795Z",
+ "start_time": "2022-01-07T19:10:30.690003Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:10:30.785887Z",
+ "start_time": "2022-01-07T19:10:30.710912Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Universal functions\n",
+ "\n",
+ "Standard mathematical functions can be calculated on any scalar, scalar-valued iterable (ranges, lists, tuples containing numbers), and on `ndarray`s without having to change the call signature. In all cases the functions return a new `ndarray` of typecode `float` (since these functions usually generate float values, anyway). The only exceptions to this rule are the `exp`, and `sqrt` functions, which, if `ULAB_SUPPORTS_COMPLEX` is set to 1 in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h), can return complex arrays, depending on the argument. All functions execute faster with `ndarray` arguments than with iterables, because the values of the input vector can be extracted faster. \n",
+ "\n",
+ "At present, the following functions are supported (starred functions can operate on, or can return complex arrays):\n",
+ "\n",
+ "`acos`, `acosh`, `arctan2`, `around`, `asin`, `asinh`, `atan`, `arctan2`, `atanh`, `ceil`, `cos`, `degrees`, `exp*`, `expm1`, `floor`, `log`, `log10`, `log2`, `radians`, `sin`, `sinh`, `sqrt*`, `tan`, `tanh`.\n",
+ "\n",
+ "These functions are applied element-wise to the arguments, thus, e.g., the exponential of a matrix cannot be calculated in this way, only the exponential of the matrix entries."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T19:11:07.579601Z",
+ "start_time": "2021-01-13T19:11:07.554672Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t range(0, 9)\n",
+ "exp(a):\t array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)\n",
+ "\n",
+ "=============\n",
+ "b:\n",
+ " array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "exp(b):\n",
+ " array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)\n",
+ "\n",
+ "=============\n",
+ "c:\n",
+ " array([[0.0, 1.0, 2.0],\n",
+ " [3.0, 4.0, 5.0],\n",
+ " [6.0, 7.0, 8.0]], dtype=float64)\n",
+ "exp(c):\n",
+ " array([[1.0, 2.718281828459045, 7.38905609893065],\n",
+ " [20.08553692318767, 54.59815003314424, 148.4131591025766],\n",
+ " [403.4287934927351, 1096.633158428459, 2980.957987041728]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = range(9)\n",
+ "b = np.array(a)\n",
+ "\n",
+ "# works with ranges, lists, tuples etc.\n",
+ "print('a:\\t', a)\n",
+ "print('exp(a):\\t', np.exp(a))\n",
+ "\n",
+ "# with 1D arrays\n",
+ "print('\\n=============\\nb:\\n', b)\n",
+ "print('exp(b):\\n', np.exp(b))\n",
+ "\n",
+ "# as well as with matrices\n",
+ "c = np.array(range(9)).reshape((3, 3))\n",
+ "print('\\n=============\\nc:\\n', c)\n",
+ "print('exp(c):\\n', np.exp(c))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Computation expenses\n",
+ "\n",
+ "The overhead for calculating with micropython iterables is quite significant: for the 1000 samples below, the difference is more than 800 microseconds, because internally the function has to create the `ndarray` for the output, has to fetch the iterable's items of unknown type, and then convert them to floats. All these steps are skipped for `ndarray`s, because these pieces of information are already known. \n",
+ "\n",
+ "Doing the same with `list` comprehension requires 30 times more time than with the `ndarray`, which would become even more, if we converted the resulting list to an `ndarray`. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 59,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:45.696282Z",
+ "start_time": "2020-05-07T07:35:45.629909Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "iterating over ndarray in ulab\r\n",
+ "execution time: 441 us\r\n",
+ "\r\n",
+ "iterating over list in ulab\r\n",
+ "execution time: 1266 us\r\n",
+ "\r\n",
+ "iterating over list in python\r\n",
+ "execution time: 11379 us\r\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "import math\n",
+ "\n",
+ "a = [0]*1000\n",
+ "b = np.array(a)\n",
+ "\n",
+ "@timeit\n",
+ "def timed_vector(iterable):\n",
+ " return np.exp(iterable)\n",
+ "\n",
+ "@timeit\n",
+ "def timed_list(iterable):\n",
+ " return [math.exp(i) for i in iterable]\n",
+ "\n",
+ "print('iterating over ndarray in ulab')\n",
+ "timed_vector(b)\n",
+ "\n",
+ "print('\\niterating over list in ulab')\n",
+ "timed_vector(a)\n",
+ "\n",
+ "print('\\niterating over list in python')\n",
+ "timed_list(a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## arctan2\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.arctan2.html\n",
+ "\n",
+ "The two-argument inverse tangent function is also part of the `vector` sub-module. The function implements broadcasting as discussed in the section on `ndarray`s. Scalars (`micropython` integers or floats) are also allowed."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T19:15:08.215912Z",
+ "start_time": "2021-01-13T19:15:08.189806Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([1.0, 2.2, 33.33, 444.444], dtype=float64)\n",
+ "\n",
+ "arctan2(a, 1.0)\n",
+ " array([0.7853981633974483, 1.14416883366802, 1.5408023243361, 1.568546328341769], dtype=float64)\n",
+ "\n",
+ "arctan2(1.0, a)\n",
+ " array([0.7853981633974483, 0.426627493126876, 0.02999400245879636, 0.002249998453127392], dtype=float64)\n",
+ "\n",
+ "arctan2(a, a): \n",
+ " array([0.7853981633974483, 0.7853981633974483, 0.7853981633974483, 0.7853981633974483], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2.2, 33.33, 444.444])\n",
+ "print('a:\\n', a)\n",
+ "print('\\narctan2(a, 1.0)\\n', np.arctan2(a, 1.0))\n",
+ "print('\\narctan2(1.0, a)\\n', np.arctan2(1.0, a))\n",
+ "print('\\narctan2(a, a): \\n', np.arctan2(a, a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## around\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.around.html\n",
+ "\n",
+ "`numpy`'s `around` function can also be found in the `vector` sub-module. The function implements the `decimals` keyword argument with default value `0`. The first argument must be an `ndarray`. If this is not the case, the function raises a `TypeError` exception. Note that `numpy` accepts general iterables. The `out` keyword argument known from `numpy` is not accepted. The function always returns an ndarray of type `mp_float_t`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T19:19:46.728823Z",
+ "start_time": "2021-01-13T19:19:46.703348Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([1.0, 2.2, 33.33, 444.444], dtype=float64)\n",
+ "\n",
+ "decimals = 0\t array([1.0, 2.0, 33.0, 444.0], dtype=float64)\n",
+ "\n",
+ "decimals = 1\t array([1.0, 2.2, 33.3, 444.4], dtype=float64)\n",
+ "\n",
+ "decimals = -1\t array([0.0, 0.0, 30.0, 440.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2.2, 33.33, 444.444])\n",
+ "print('a:\\t\\t', a)\n",
+ "print('\\ndecimals = 0\\t', np.around(a, decimals=0))\n",
+ "print('\\ndecimals = 1\\t', np.around(a, decimals=1))\n",
+ "print('\\ndecimals = -1\\t', np.around(a, decimals=-1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## exp\n",
+ "\n",
+ "If `ULAB_SUPPORTS_COMPLEX` is set to 1 in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h), the exponential function can also take complex arrays as its argument, in which case the return value is also complex."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:41:51.865779Z",
+ "start_time": "2022-01-07T18:41:51.843897Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([1.0, 2.0, 3.0], dtype=float64)\n",
+ "exp(a):\t\t array([2.718281828459045, 7.38905609893065, 20.08553692318767], dtype=float64)\n",
+ "\n",
+ "b:\t\t array([1.0+1.0j, 2.0+2.0j, 3.0+3.0j], dtype=complex)\n",
+ "exp(b):\t\t array([1.468693939915885+2.287355287178842j, -3.074932320639359+6.71884969742825j, -19.88453084414699+2.834471132487004j], dtype=complex)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3])\n",
+ "print('a:\\t\\t', a)\n",
+ "print('exp(a):\\t\\t', np.exp(a))\n",
+ "print()\n",
+ "\n",
+ "b = np.array([1+1j, 2+2j, 3+3j], dtype=np.complex)\n",
+ "print('b:\\t\\t', b)\n",
+ "print('exp(b):\\t\\t', np.exp(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sqrt\n",
+ "\n",
+ "If `ULAB_SUPPORTS_COMPLEX` is set to 1 in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h), the exponential function can also take complex arrays as its argument, in which case the return value is also complex. If the input is real, but the results might be complex, the user is supposed to specify the output `dtype` in the function call. Otherwise, the square roots of negative numbers will result in `NaN`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:45:26.554520Z",
+ "start_time": "2022-01-07T18:45:26.543552Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([1.0, -1.0], dtype=float64)\n",
+ "sqrt(a):\t\t array([1.0, nan], dtype=float64)\n",
+ "sqrt(a):\t\t array([1.0+0.0j, 0.0+1.0j], dtype=complex)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, -1])\n",
+ "print('a:\\t\\t', a)\n",
+ "print('sqrt(a):\\t\\t', np.sqrt(a))\n",
+ "print('sqrt(a):\\t\\t', np.sqrt(a, dtype=np.complex))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Vectorising generic python functions\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html\n",
+ "\n",
+ "The examples above use factory functions. In fact, they are nothing but the vectorised versions of the standard mathematical functions. User-defined `python` functions can also be vectorised by help of `vectorize`. This function takes a positional argument, namely, the `python` function that you want to vectorise, and a non-mandatory keyword argument, `otypes`, which determines the `dtype` of the output array. The `otypes` must be `None` (default), or any of the `dtypes` defined in `ulab`. With `None`, the output is automatically turned into a float array. \n",
+ "\n",
+ "The return value of `vectorize` is a `micropython` object that can be called as a standard function, but which now accepts either a scalar, an `ndarray`, or a generic `micropython` iterable as its sole argument. Note that the function that is to be vectorised must have a single argument."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T19:16:55.709617Z",
+ "start_time": "2021-01-13T19:16:55.688222Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "f on a scalar: array([1936.0], dtype=float64)\n",
+ "f on an ndarray: array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n",
+ "f on a list: array([4.0, 9.0, 16.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "def f(x):\n",
+ " return x*x\n",
+ "\n",
+ "vf = np.vectorize(f)\n",
+ "\n",
+ "# calling with a scalar\n",
+ "print('{:20}'.format('f on a scalar: '), vf(44.0))\n",
+ "\n",
+ "# calling with an ndarray\n",
+ "a = np.array([1, 2, 3, 4])\n",
+ "print('{:20}'.format('f on an ndarray: '), vf(a))\n",
+ "\n",
+ "# calling with a list\n",
+ "print('{:20}'.format('f on a list: '), vf([2, 3, 4]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As mentioned, the `dtype` of the resulting `ndarray` can be specified via the `otypes` keyword. The value is bound to the function object that `vectorize` returns, therefore, if the same function is to be vectorised with different output types, then for each type a new function object must be created."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T19:19:36.090837Z",
+ "start_time": "2021-01-13T19:19:36.069088Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "output is uint8: array([1, 4, 9, 16], dtype=uint8)\n",
+ "output is float: array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "l = [1, 2, 3, 4]\n",
+ "def f(x):\n",
+ " return x*x\n",
+ "\n",
+ "vf1 = np.vectorize(f, otypes=np.uint8)\n",
+ "vf2 = np.vectorize(f, otypes=np.float)\n",
+ "\n",
+ "print('{:20}'.format('output is uint8: '), vf1(l))\n",
+ "print('{:20}'.format('output is float: '), vf2(l))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `otypes` keyword argument cannot be used for type coercion: if the function evaluates to a float, but `otypes` would dictate an integer type, an exception will be raised:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-06T22:21:43.616220Z",
+ "start_time": "2020-05-06T22:21:43.601280Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "integer list: array([1, 4, 9, 16], dtype=uint8)\n",
+ "\n",
+ "Traceback (most recent call last):\n",
+ " File \"/dev/shm/micropython.py\", line 14, in <module>\n",
+ "TypeError: can't convert float to int\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "int_list = [1, 2, 3, 4]\n",
+ "float_list = [1.0, 2.0, 3.0, 4.0]\n",
+ "def f(x):\n",
+ " return x*x\n",
+ "\n",
+ "vf = np.vectorize(f, otypes=np.uint8)\n",
+ "\n",
+ "print('{:20}'.format('integer list: '), vf(int_list))\n",
+ "# this will raise a TypeError exception\n",
+ "print(vf(float_list))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Benchmarks\n",
+ "\n",
+ "It should be pointed out that the `vectorize` function produces the pseudo-vectorised version of the `python` function that is fed into it, i.e., on the C level, the same `python` function is called, with the all-encompassing `mp_obj_t` type arguments, and all that happens is that the `for` loop in `[f(i) for i in iterable]` runs purely in C. Since type checking and type conversion in `f()` is expensive, the speed-up is not so spectacular as when iterating over an `ndarray` with a factory function: a gain of approximately 30% can be expected, when a native `python` type (e.g., `list`) is returned by the function, and this becomes around 50% (a factor of 2), if conversion to an `ndarray` is also counted.\n",
+ "\n",
+ "The following code snippet calculates the square of a 1000 numbers with the vectorised function (which returns an `ndarray`), with `list` comprehension, and with `list` comprehension followed by conversion to an `ndarray`. For comparison, the execution time is measured also for the case, when the square is calculated entirely in `ulab`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 45,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:32:20.048553Z",
+ "start_time": "2020-05-07T07:32:19.951851Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "vectorised function\r\n",
+ "execution time: 7237 us\r\n",
+ "\r\n",
+ "list comprehension\r\n",
+ "execution time: 10248 us\r\n",
+ "\r\n",
+ "list comprehension + ndarray conversion\r\n",
+ "execution time: 12562 us\r\n",
+ "\r\n",
+ "squaring an ndarray entirely in ulab\r\n",
+ "execution time: 560 us\r\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "def f(x):\n",
+ " return x*x\n",
+ "\n",
+ "vf = np.vectorize(f)\n",
+ "\n",
+ "@timeit\n",
+ "def timed_vectorised_square(iterable):\n",
+ " return vf(iterable)\n",
+ "\n",
+ "@timeit\n",
+ "def timed_python_square(iterable):\n",
+ " return [f(i) for i in iterable]\n",
+ "\n",
+ "@timeit\n",
+ "def timed_ndarray_square(iterable):\n",
+ " return np.array([f(i) for i in iterable])\n",
+ "\n",
+ "@timeit\n",
+ "def timed_ulab_square(ndarray):\n",
+ " return ndarray**2\n",
+ "\n",
+ "print('vectorised function')\n",
+ "squares = timed_vectorised_square(range(1000))\n",
+ "\n",
+ "print('\\nlist comprehension')\n",
+ "squares = timed_python_square(range(1000))\n",
+ "\n",
+ "print('\\nlist comprehension + ndarray conversion')\n",
+ "squares = timed_ndarray_square(range(1000))\n",
+ "\n",
+ "print('\\nsquaring an ndarray entirely in ulab')\n",
+ "a = np.array(range(1000))\n",
+ "squares = timed_ulab_square(a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "From the comparisons above, it is obvious that `python` functions should only be vectorised, when the same effect cannot be gotten in `ulab` only. However, although the time savings are not significant, there is still a good reason for caring about vectorised functions. Namely, user-defined `python` functions become universal, i.e., they can accept generic iterables as well as `ndarray`s as their arguments. A vectorised function is still a one-liner, resulting in transparent and elegant code.\n",
+ "\n",
+ "A final comment on this subject: the `f(x)` that we defined is a *generic* `python` function. This means that it is not required that it just crunches some numbers. It has to return a number object, but it can still access the hardware in the meantime. So, e.g., \n",
+ "\n",
+ "```python\n",
+ "\n",
+ "led = pyb.LED(2)\n",
+ "\n",
+ "def f(x):\n",
+ " if x < 100:\n",
+ " led.toggle()\n",
+ " return x*x\n",
+ "```\n",
+ "\n",
+ "is perfectly valid code."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/scipy-linalg.ipynb b/circuitpython/extmod/ulab/docs/scipy-linalg.ipynb
new file mode 100644
index 0000000..6adaa11
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/scipy-linalg.ipynb
@@ -0,0 +1,474 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:54:58.722373Z",
+ "start_time": "2021-01-13T18:54:57.178438Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-05-09T05:37:22.600510Z",
+ "start_time": "2021-05-09T05:37:22.595924Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-05-09T05:37:26.429136Z",
+ "start_time": "2021-05-09T05:37:26.403191Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# scipy.linalg\n",
+ "\n",
+ "`scipy`'s `linalg` module contains two functions, `solve_triangular`, and `cho_solve`. The functions can be called by prepending them by `scipy.linalg.`.\n",
+ "\n",
+ "1. [scipy.linalg.solve_cho](#cho_solve)\n",
+ "2. [scipy.linalg.solve_triangular](#solve_triangular)"
+ ]
+ },
+ {
+ "source": [
+ "## cho_solve\n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.cho_solve.html\n",
+ "\n",
+ "Solve the linear equations \n",
+ "\n",
+ "\n",
+ "\\begin{equation}\n",
+ "\\mathbf{A}\\cdot\\mathbf{x} = \\mathbf{b}\n",
+ "\\end{equation}\n",
+ "\n",
+ "given the Cholesky factorization of $\\mathbf{A}$. As opposed to `scipy`, the function simply takes the Cholesky-factorised matrix, $\\mathbf{A}$, and $\\mathbf{b}$ as inputs."
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "array([-0.01388888888888906, -0.6458333333333331, 2.677083333333333, -0.01041666666666667], dtype=float64)\n\n\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "A = np.array([[3, 0, 0, 0], [2, 1, 0, 0], [1, 0, 1, 0], [1, 2, 1, 8]])\n",
+ "b = np.array([4, 2, 4, 2])\n",
+ "\n",
+ "print(spy.linalg.cho_solve(A, b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## solve_triangular\n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve_triangular.html \n",
+ "\n",
+ "Solve the linear equation \n",
+ "\n",
+ "\\begin{equation}\n",
+ "\\mathbf{a}\\cdot\\mathbf{x} = \\mathbf{b}\n",
+ "\\end{equation}\n",
+ "\n",
+ "with the assumption that $\\mathbf{a}$ is a triangular matrix. The two position arguments are $\\mathbf{a}$, and $\\mathbf{b}$, and the optional keyword argument is `lower` with a default value of `False`. `lower` determines, whether data are taken from the lower, or upper triangle of $\\mathbf{a}$. \n",
+ "\n",
+ "Note that $\\mathbf{a}$ itself does not have to be a triangular matrix: if it is not, then the values are simply taken to be 0 in the upper or lower triangle, as dictated by `lower`. However, $\\mathbf{a}\\cdot\\mathbf{x}$ will yield $\\mathbf{b}$ only, when $\\mathbf{a}$ is triangular. You should keep this in mind, when trying to establish the validity of the solution by back substitution."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-05-09T05:56:57.449996Z",
+ "start_time": "2021-05-09T05:56:57.422515Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ "\n",
+ "array([[3.0, 0.0, 0.0, 0.0],\n",
+ " [2.0, 1.0, 0.0, 0.0],\n",
+ " [1.0, 0.0, 1.0, 0.0],\n",
+ " [1.0, 2.0, 1.0, 8.0]], dtype=float64)\n",
+ "\n",
+ "b: array([4.0, 2.0, 4.0, 2.0], dtype=float64)\n",
+ "====================\n",
+ "x: array([1.333333333333333, -0.6666666666666665, 2.666666666666667, -0.08333333333333337], dtype=float64)\n",
+ "\n",
+ "dot(a, x): array([4.0, 2.0, 4.0, 2.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "a = np.array([[3, 0, 0, 0], [2, 1, 0, 0], [1, 0, 1, 0], [1, 2, 1, 8]])\n",
+ "b = np.array([4, 2, 4, 2])\n",
+ "\n",
+ "print('a:\\n')\n",
+ "print(a)\n",
+ "print('\\nb: ', b)\n",
+ "\n",
+ "x = spy.linalg.solve_triangular(a, b, lower=True)\n",
+ "\n",
+ "print('='*20)\n",
+ "print('x: ', x)\n",
+ "print('\\ndot(a, x): ', np.dot(a, x))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "With get the same solution, $\\mathbf{x}$, with the following matrix, but the dot product of $\\mathbf{a}$, and $\\mathbf{x}$ is no longer $\\mathbf{b}$:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-05-09T06:03:30.853054Z",
+ "start_time": "2021-05-09T06:03:30.841500Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ "\n",
+ "array([[3.0, 2.0, 1.0, 0.0],\n",
+ " [2.0, 1.0, 0.0, 1.0],\n",
+ " [1.0, 0.0, 1.0, 4.0],\n",
+ " [1.0, 2.0, 1.0, 8.0]], dtype=float64)\n",
+ "\n",
+ "b: array([4.0, 2.0, 4.0, 2.0], dtype=float64)\n",
+ "====================\n",
+ "x: array([1.333333333333333, -0.6666666666666665, 2.666666666666667, -0.08333333333333337], dtype=float64)\n",
+ "\n",
+ "dot(a, x): array([5.333333333333334, 1.916666666666666, 3.666666666666667, 2.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "a = np.array([[3, 2, 1, 0], [2, 1, 0, 1], [1, 0, 1, 4], [1, 2, 1, 8]])\n",
+ "b = np.array([4, 2, 4, 2])\n",
+ "\n",
+ "print('a:\\n')\n",
+ "print(a)\n",
+ "print('\\nb: ', b)\n",
+ "\n",
+ "x = spy.linalg.solve_triangular(a, b, lower=True)\n",
+ "\n",
+ "print('='*20)\n",
+ "print('x: ', x)\n",
+ "print('\\ndot(a, x): ', np.dot(a, x))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+} \ No newline at end of file
diff --git a/circuitpython/extmod/ulab/docs/scipy-optimize.ipynb b/circuitpython/extmod/ulab/docs/scipy-optimize.ipynb
new file mode 100644
index 0000000..eea97b7
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/scipy-optimize.ipynb
@@ -0,0 +1,515 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:50:51.417613Z",
+ "start_time": "2021-01-08T12:50:51.208257Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:50:52.581876Z",
+ "start_time": "2021-01-08T12:50:52.567901Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:50:53.516712Z",
+ "start_time": "2021-01-08T12:50:53.454984Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# scipy.optimize\n",
+ "\n",
+ "Functions in the `optimize` module can be called by prepending them by `scipy.optimize.`. The module defines the following three functions:\n",
+ "\n",
+ "1. [scipy.optimize.bisect](#bisect)\n",
+ "1. [scipy.optimize.fmin](#fmin)\n",
+ "1. [scipy.optimize.newton](#newton)\n",
+ "\n",
+ "Note that routines that work with user-defined functions still have to call the underlying `python` code, and therefore, gains in speed are not as significant as with other vectorised operations. As a rule of thumb, a factor of two can be expected, when compared to an optimised `python` implementation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## bisect \n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.bisect.html\n",
+ "\n",
+ "`bisect` finds the root of a function of one variable using a simple bisection routine. It takes three positional arguments, the function itself, and two starting points. The function must have opposite signs\n",
+ "at the starting points. Returned is the position of the root.\n",
+ "\n",
+ "Two keyword arguments, `xtol`, and `maxiter` can be supplied to control the accuracy, and the number of bisections, respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:58:28.444300Z",
+ "start_time": "2021-01-08T12:58:28.421989Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.9999997615814209\n",
+ "only 8 bisections: 0.984375\n",
+ "with 0.1 accuracy: 0.9375\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ " \n",
+ "def f(x):\n",
+ " return x*x - 1\n",
+ "\n",
+ "print(spy.optimize.bisect(f, 0, 4))\n",
+ "\n",
+ "print('only 8 bisections: ', spy.optimize.bisect(f, 0, 4, maxiter=8))\n",
+ "\n",
+ "print('with 0.1 accuracy: ', spy.optimize.bisect(f, 0, 4, xtol=0.1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Performance\n",
+ "\n",
+ "Since the `bisect` routine calls user-defined `python` functions, the speed gain is only about a factor of two, if compared to a purely `python` implementation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:08:24.750562Z",
+ "start_time": "2020-05-19T19:08:24.682959Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "bisect running in python\r\n",
+ "execution time: 1270 us\r\n",
+ "bisect running in C\r\n",
+ "execution time: 642 us\r\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "def f(x):\n",
+ " return (x-1)*(x-1) - 2.0\n",
+ "\n",
+ "def bisect(f, a, b, xtol=2.4e-7, maxiter=100):\n",
+ " if f(a) * f(b) > 0:\n",
+ " raise ValueError\n",
+ "\n",
+ " rtb = a if f(a) < 0.0 else b\n",
+ " dx = b - a if f(a) < 0.0 else a - b\n",
+ " for i in range(maxiter):\n",
+ " dx *= 0.5\n",
+ " x_mid = rtb + dx\n",
+ " mid_value = f(x_mid)\n",
+ " if mid_value < 0:\n",
+ " rtb = x_mid\n",
+ " if abs(dx) < xtol:\n",
+ " break\n",
+ "\n",
+ " return rtb\n",
+ "\n",
+ "@timeit\n",
+ "def bisect_scipy(f, a, b):\n",
+ " return spy.optimize.bisect(f, a, b)\n",
+ "\n",
+ "@timeit\n",
+ "def bisect_timed(f, a, b):\n",
+ " return bisect(f, a, b)\n",
+ "\n",
+ "print('bisect running in python')\n",
+ "bisect_timed(f, 3, 2)\n",
+ "\n",
+ "print('bisect running in C')\n",
+ "bisect_scipy(f, 3, 2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## fmin\n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin.html\n",
+ "\n",
+ "The `fmin` function finds the position of the minimum of a user-defined function by using the downhill simplex method. Requires two positional arguments, the function, and the initial value. Three keyword arguments, `xatol`, `fatol`, and `maxiter` stipulate conditions for stopping."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:00:26.729947Z",
+ "start_time": "2021-01-08T13:00:26.702748Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.9996093749999952\n",
+ "1.199999999999996\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "def f(x):\n",
+ " return (x-1)**2 - 1\n",
+ "\n",
+ "print(spy.optimize.fmin(f, 3.0))\n",
+ "print(spy.optimize.fmin(f, 3.0, xatol=0.1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## newton\n",
+ "\n",
+ "`scipy`:https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton.html\n",
+ "\n",
+ "`newton` finds a zero of a real, user-defined function using the Newton-Raphson (or secant or Halley’s) method. The routine requires two positional arguments, the function, and the initial value. Three keyword\n",
+ "arguments can be supplied to control the iteration. These are the absolute and relative tolerances `tol`, and `rtol`, respectively, and the number of iterations before stopping, `maxiter`. The function retuns a single scalar, the position of the root."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:56:35.139958Z",
+ "start_time": "2021-01-08T12:56:35.119712Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1.260135727246117\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ " \n",
+ "def f(x):\n",
+ " return x*x*x - 2.0\n",
+ "\n",
+ "print(spy.optimize.newton(f, 3., tol=0.001, rtol=0.01))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/scipy-signal.ipynb b/circuitpython/extmod/ulab/docs/scipy-signal.ipynb
new file mode 100644
index 0000000..c4d5f85
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/scipy-signal.ipynb
@@ -0,0 +1,482 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:11:12.111639Z",
+ "start_time": "2021-01-12T16:11:11.914041Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:20:19.755153Z",
+ "start_time": "2022-01-07T19:20:19.745524Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:20:27.595871Z",
+ "start_time": "2022-01-07T19:20:27.565514Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# scipy.signal\n",
+ "\n",
+ "Functions in the `signal` module can be called by prepending them by `scipy.signal.`. The module defines the following two functions:\n",
+ "\n",
+ "1. [scipy.signal.sosfilt](#sosfilt)\n",
+ "1. [scipy.signal.spectrogram](#spectrogram)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sosfilt\n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.sosfilt.html \n",
+ "\n",
+ "Filter data along one dimension using cascaded second-order sections.\n",
+ "\n",
+ "The function takes two positional arguments, `sos`, the filter segments of length 6, and the one-dimensional, uniformly sampled data set to be filtered. Returns the filtered data, or the filtered data and the final filter delays, if the `zi` keyword arguments is supplied. The keyword argument must be a float `ndarray` of shape `(n_sections, 2)`. If `zi` is not passed to the function, the initial values are assumed to be 0."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-06-19T20:24:10.529668Z",
+ "start_time": "2020-06-19T20:24:10.520389Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "y: array([0.0, 1.0, -4.0, 24.0, -104.0, 440.0, -1728.0, 6532.000000000001, -23848.0, 84864.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])\n",
+ "sos = [[1, 2, 3, 1, 5, 6], [1, 2, 3, 1, 5, 6]]\n",
+ "y = spy.signal.sosfilt(sos, x)\n",
+ "print('y: ', y)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-06-19T20:27:39.508508Z",
+ "start_time": "2020-06-19T20:27:39.498256Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "y: array([4.0, -16.0, 63.00000000000001, -227.0, 802.9999999999999, -2751.0, 9271.000000000001, -30775.0, 101067.0, -328991.0000000001], dtype=float)\n",
+ "\n",
+ "========================================\n",
+ "zf: array([[37242.0, 74835.0],\n",
+ "\t [1026187.0, 1936542.0]], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])\n",
+ "sos = [[1, 2, 3, 1, 5, 6], [1, 2, 3, 1, 5, 6]]\n",
+ "# initial conditions of the filter\n",
+ "zi = np.array([[1, 2], [3, 4]])\n",
+ "\n",
+ "y, zf = spy.signal.sosfilt(sos, x, zi=zi)\n",
+ "print('y: ', y)\n",
+ "print('\\n' + '='*40 + '\\nzf: ', zf)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## spectrogram\n",
+ "\n",
+ "In addition to the Fourier transform and its inverse, `ulab` also sports a function called `spectrogram`, which returns the absolute value of the Fourier transform. This could be used to find the dominant spectral component in a time series. The arguments are treated in the same way as in `fft`, and `ifft`. This means that, if the firmware was compiled with complex support, the input can also be a complex array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:12:06.573408Z",
+ "start_time": "2021-01-12T16:12:06.560558Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "original vector:\t array([0.0, 0.009775015390171337, 0.01954909674625918, ..., -0.5275140569487312, -0.5357931822978732, -0.5440211108893639], dtype=float64)\n",
+ "\n",
+ "spectrum:\t array([187.8635087634579, 315.3112063607119, 347.8814873399374, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "x = np.linspace(0, 10, num=1024)\n",
+ "y = np.sin(x)\n",
+ "\n",
+ "a = spy.signal.spectrogram(y)\n",
+ "\n",
+ "print('original vector:\\t', y)\n",
+ "print('\\nspectrum:\\t', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As such, `spectrogram` is really just a shorthand for `np.sqrt(a*a + b*b)`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:13:36.726662Z",
+ "start_time": "2021-01-12T16:13:36.705036Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "spectrum calculated the hard way:\t array([187.8635087634579, 315.3112063607119, 347.8814873399374, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)\n",
+ "\n",
+ "spectrum calculated the lazy way:\t array([187.8635087634579, 315.3112063607119, 347.8814873399374, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "x = np.linspace(0, 10, num=1024)\n",
+ "y = np.sin(x)\n",
+ "\n",
+ "a, b = np.fft.fft(y)\n",
+ "\n",
+ "print('\\nspectrum calculated the hard way:\\t', np.sqrt(a*a + b*b))\n",
+ "\n",
+ "a = spy.signal.spectrogram(y)\n",
+ "\n",
+ "print('\\nspectrum calculated the lazy way:\\t', a)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/scipy-special.ipynb b/circuitpython/extmod/ulab/docs/scipy-special.ipynb
new file mode 100644
index 0000000..c3a0cf8
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/scipy-special.ipynb
@@ -0,0 +1,344 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:54:58.722373Z",
+ "start_time": "2021-01-13T18:54:57.178438Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:57:41.555892Z",
+ "start_time": "2021-01-13T18:57:41.551121Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T18:57:42.313231Z",
+ "start_time": "2021-01-13T18:57:42.288402Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# scipy.special\n",
+ "\n",
+ "`scipy`'s `special` module defines several functions that behave as do the standard mathematical functions of the `numpy`, i.e., they can be called on any scalar, scalar-valued iterable (ranges, lists, tuples containing numbers), and on `ndarray`s without having to change the call signature. In all cases the functions return a new `ndarray` of typecode `float` (since these functions usually generate float values, anyway). \n",
+ "\n",
+ "At present, `ulab`'s `special` module contains the following functions:\n",
+ "\n",
+ "`erf`, `erfc`, `gamma`, and `gammaln`, and they can be called by prepending them by `scipy.special.`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T19:06:54.640444Z",
+ "start_time": "2021-01-13T19:06:54.623467Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: range(0, 9)\n",
+ "array([0.0, 0.8427007929497149, 0.9953222650189527, 0.9999779095030014, 0.9999999845827421, 1.0, 1.0, 1.0, 1.0], dtype=float64)\n",
+ "\n",
+ "b: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "array([1.0, 0.1572992070502851, 0.004677734981047265, 2.209049699858544e-05, 1.541725790028002e-08, 1.537459794428035e-12, 2.151973671249892e-17, 4.183825607779414e-23, 1.122429717298293e-29], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "a = range(9)\n",
+ "b = np.array(a)\n",
+ "\n",
+ "print('a: ', a)\n",
+ "print(spy.special.erf(a))\n",
+ "\n",
+ "print('\\nb: ', b)\n",
+ "print(spy.special.erfc(b))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/templates/manual.tpl b/circuitpython/extmod/ulab/docs/templates/manual.tpl
new file mode 100644
index 0000000..ba6b73e
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/templates/manual.tpl
@@ -0,0 +1,113 @@
+
+{%- extends 'display_priority.tpl' -%}
+
+
+{% block in_prompt %}
+{% endblock in_prompt %}
+
+{% block output_prompt %}
+{% endblock output_prompt %}
+
+{% block input scoped%}
+
+{%- if cell.source.split('\n')[0].startswith('%%micropython') -%}
+.. code::
+
+{{ '\n'.join(['# code to be run in micropython'] + cell.source.strip().split('\n')[1:]) | indent}}
+
+{%- else -%}
+.. code::
+
+{{ '\n'.join(['# code to be run in CPython\n'] + cell.source.strip().split('\n')) | indent}}
+{%- endif -%}
+{% endblock input %}
+
+{% block error %}
+::
+
+{{ super() }}
+{% endblock error %}
+
+{% block traceback_line %}
+{{ line | indent | strip_ansi }}
+{% endblock traceback_line %}
+
+{% block execute_result %}
+{% block data_priority scoped %}
+{{ super() }}
+{% endblock %}
+{% endblock execute_result %}
+
+{% block stream %}
+.. parsed-literal::
+
+{{ output.text | indent }}
+{% endblock stream %}
+
+{% block data_svg %}
+.. image:: {{ output.metadata.filenames['image/svg+xml'] | urlencode }}
+{% endblock data_svg %}
+
+{% block data_png %}
+.. image:: {{ output.metadata.filenames['image/png'] | urlencode }}
+{%- set width=output | get_metadata('width', 'image/png') -%}
+{%- if width is not none %}
+ :width: {{ width }}px
+{%- endif %}
+{%- set height=output | get_metadata('height', 'image/png') -%}
+{%- if height is not none %}
+ :height: {{ height }}px
+{%- endif %}
+{% endblock data_png %}
+
+{% block data_jpg %}
+.. image:: {{ output.metadata.filenames['image/jpeg'] | urlencode }}
+{%- set width=output | get_metadata('width', 'image/jpeg') -%}
+{%- if width is not none %}
+ :width: {{ width }}px
+{%- endif %}
+{%- set height=output | get_metadata('height', 'image/jpeg') -%}
+{%- if height is not none %}
+ :height: {{ height }}px
+{%- endif %}
+{% endblock data_jpg %}
+
+{% block data_markdown %}
+{{ output.data['text/markdown'] | convert_pandoc("markdown", "rst") }}
+{% endblock data_markdown %}
+
+{% block data_latex %}
+.. math::
+
+{{ output.data['text/latex'] | strip_dollars | indent }}
+{% endblock data_latex %}
+
+{% block data_text scoped %}
+.. parsed-literal::
+
+{{ output.data['text/plain'] | indent }}
+{% endblock data_text %}
+
+{% block data_html scoped %}
+.. raw:: html
+
+{{ output.data['text/html'] | indent }}
+{% endblock data_html %}
+
+{% block markdowncell scoped %}
+{{ cell.source | convert_pandoc("markdown", "rst") }}
+{% endblock markdowncell %}
+
+{%- block rawcell scoped -%}
+{%- if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) %}
+{{cell.source}}
+{% endif -%}
+{%- endblock rawcell -%}
+
+{% block headingcell scoped %}
+{{ ("#" * cell.level + cell.source) | replace('\n', ' ') | convert_pandoc("markdown", "rst") }}
+{% endblock headingcell %}
+
+{% block unknowncell scoped %}
+unknown type {{cell.type}}
+{% endblock unknowncell %}
diff --git a/circuitpython/extmod/ulab/docs/templates/rst.tpl b/circuitpython/extmod/ulab/docs/templates/rst.tpl
new file mode 100644
index 0000000..479a69f
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/templates/rst.tpl
@@ -0,0 +1,144 @@
+
+{%- extends 'display_priority.tpl' -%}
+
+
+{% block in_prompt %}
+{% endblock in_prompt %}
+
+{% block output_prompt %}
+{% endblock output_prompt %}
+
+{% block input scoped%}
+
+{%- if '%%ccode' in cell.source.strip().split('\n')[0] -%}
+
+{{ 'https://github.com/v923z/micropython-ulab/tree/master/code/' + cell.source.strip().split('\n')[0].split()[-1] }}
+
+.. code:: cpp
+
+{{ '\n'.join( cell.source.strip().split('\n')[1:] ) | indent }}
+
+{%- elif '%%makefile' in cell.source.strip().split('\n')[0] -%}
+
+{{ 'https://github.com/v923z/micropython-ulab/tree/master/code/' + cell.source.strip().split('\n')[0].split()[-1].split('/')[1] + '/micropython.mk' }}
+
+.. code:: make
+
+{{ '\n'.join( cell.source.strip().split('\n')[1:] ) | indent }}
+
+{%- elif cell.source.strip().split('\n')[0].startswith('!') -%}
+
+.. code:: bash
+
+{{ cell.source | indent }}
+
+{%- else -%}
+{%- if 'magics_language' in cell.metadata -%}
+ {{ cell.metadata.magics_language}}
+{%- elif 'pygments_lexer' in nb.metadata.get('language_info', {}) -%}
+ {{ nb.metadata.language_info.pygments_lexer }}
+{%- elif 'name' in nb.metadata.get('language_info', {}) -%}
+ {{ nb.metadata.language_info.name }}
+{%- endif -%}
+
+.. code ::
+
+{{ cell.source | indent}}
+{%- endif -%}
+{% endblock input %}
+
+{% block error %}
+::
+
+{{ super() }}
+{% endblock error %}
+
+{% block traceback_line %}
+{{ line | indent | strip_ansi }}
+{% endblock traceback_line %}
+
+{% block execute_result %}
+{% block data_priority scoped %}
+{{ super() }}
+{% endblock %}
+{% endblock execute_result %}
+
+{% block stream %}
+{%- if '%%ccode' in cell.source.strip().split('\n')[0] -%}
+{%- else -%}
+
+.. parsed-literal::
+
+{{ output.text | indent }}
+{%- endif -%}
+{% endblock stream %}
+
+{% block data_svg %}
+.. image:: {{ output.metadata.filenames['image/svg+xml'] | urlencode }}
+{% endblock data_svg %}
+
+{% block data_png %}
+.. image:: {{ output.metadata.filenames['image/png'] | urlencode }}
+{%- set width=output | get_metadata('width', 'image/png') -%}
+{%- if width is not none %}
+ :width: {{ width }}px
+{%- endif %}
+{%- set height=output | get_metadata('height', 'image/png') -%}
+{%- if height is not none %}
+ :height: {{ height }}px
+{%- endif %}
+{% endblock data_png %}
+
+{% block data_jpg %}
+.. image:: {{ output.metadata.filenames['image/jpeg'] | urlencode }}
+{%- set width=output | get_metadata('width', 'image/jpeg') -%}
+{%- if width is not none %}
+ :width: {{ width }}px
+{%- endif %}
+{%- set height=output | get_metadata('height', 'image/jpeg') -%}
+{%- if height is not none %}
+ :height: {{ height }}px
+{%- endif %}
+{% endblock data_jpg %}
+
+{% block data_markdown %}
+{{ output.data['text/markdown'] | convert_pandoc("markdown", "rst") }}
+{% endblock data_markdown %}
+
+{% block data_latex %}
+.. math::
+
+{{ output.data['text/latex'] | strip_dollars | indent }}
+{% endblock data_latex %}
+
+{% block data_text scoped %}
+
+.. parsed-literal::
+
+{{ output.data['text/plain'] | indent }}
+{% endblock data_text %}
+
+{% block data_html scoped %}
+.. raw:: html
+
+{{ output.data['text/html'] | indent }}
+{% endblock data_html %}
+
+{% block markdowncell scoped %}
+{{ cell.source | convert_pandoc("markdown", "rst") }}
+{% endblock markdowncell %}
+
+{%- block rawcell scoped -%}
+{%- if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) %}
+{{cell.source}}
+{% endif -%}
+{%- endblock rawcell -%}
+
+{% block headingcell scoped %}
+{{ ("#" * cell.level + cell.source) | replace('\n', ' ') | convert_pandoc("markdown", "rst") }}
+
+{% endblock headingcell %}
+
+{% block unknowncell scoped %}
+unknown type {{cell.type}}
+{% endblock unknowncell %}
diff --git a/circuitpython/extmod/ulab/docs/ulab-approx.ipynb b/circuitpython/extmod/ulab/docs/ulab-approx.ipynb
new file mode 100644
index 0000000..52dc205
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-approx.ipynb
@@ -0,0 +1,613 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:50:51.417613Z",
+ "start_time": "2021-01-08T12:50:51.208257Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:50:52.581876Z",
+ "start_time": "2021-01-08T12:50:52.567901Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:50:53.516712Z",
+ "start_time": "2021-01-08T12:50:53.454984Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Approximation methods\n",
+ "\n",
+ "`ulab` implements five functions that can be used for interpolating, root finding, and minimising arbitrary `python` functions in one dimension. Two of these functions, namely, `interp`, and `trapz` are defined in `numpy`, while the other three are parts of `scipy`'s `optimize` module. \n",
+ "\n",
+ "Note that routines that work with user-defined functions still have to call the underlying `python` code, and therefore, gains in speed are not as significant as with other vectorised operations. As a rule of thumb, a factor of two can be expected, when compared to an optimised `python` implementation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## interp\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/numpy.interp\n",
+ "\n",
+ "The `interp` function returns the linearly interpolated values of a one-dimensional numerical array. It requires three positional arguments,`x`, at which the interpolated values are evaluated, `xp`, the array\n",
+ "of the independent data variable, and `fp`, the array of the dependent values of the data. `xp` must be a monotonically increasing sequence of numbers.\n",
+ "\n",
+ "Two keyword arguments, `left`, and `right` can also be supplied; these determine the return values, if `x < xp[0]`, and `x > xp[-1]`, respectively. If these arguments are not supplied, `left`, and `right` default to `fp[0]`, and `fp[-1]`, respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:54:58.895801Z",
+ "start_time": "2021-01-08T12:54:58.869338Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([0.8, 1.8, 2.8, 3.8, 4.8], dtype=float64)\n",
+ "array([1.0, 1.8, 2.8, 4.6, 5.0], dtype=float64)\n",
+ "array([0.0, 1.8, 2.8, 4.6, 5.0], dtype=float64)\n",
+ "array([1.0, 1.8, 2.8, 4.6, 10.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.array([1, 2, 3, 4, 5]) - 0.2\n",
+ "xp = np.array([1, 2, 3, 4])\n",
+ "fp = np.array([1, 2, 3, 5])\n",
+ "\n",
+ "print(x)\n",
+ "print(np.interp(x, xp, fp))\n",
+ "print(np.interp(x, xp, fp, left=0.0))\n",
+ "print(np.interp(x, xp, fp, right=10.0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## newton\n",
+ "\n",
+ "`scipy`:https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton.html\n",
+ "\n",
+ "`newton` finds a zero of a real, user-defined function using the Newton-Raphson (or secant or Halley’s) method. The routine requires two positional arguments, the function, and the initial value. Three keyword\n",
+ "arguments can be supplied to control the iteration. These are the absolute and relative tolerances `tol`, and `rtol`, respectively, and the number of iterations before stopping, `maxiter`. The function retuns a single scalar, the position of the root."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:56:35.139958Z",
+ "start_time": "2021-01-08T12:56:35.119712Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1.260135727246117\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ " \n",
+ "def f(x):\n",
+ " return x*x*x - 2.0\n",
+ "\n",
+ "print(spy.optimize.newton(f, 3., tol=0.001, rtol=0.01))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## bisect \n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.bisect.html\n",
+ "\n",
+ "`bisect` finds the root of a function of one variable using a simple bisection routine. It takes three positional arguments, the function itself, and two starting points. The function must have opposite signs\n",
+ "at the starting points. Returned is the position of the root.\n",
+ "\n",
+ "Two keyword arguments, `xtol`, and `maxiter` can be supplied to control the accuracy, and the number of bisections, respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:58:28.444300Z",
+ "start_time": "2021-01-08T12:58:28.421989Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.9999997615814209\n",
+ "only 8 bisections: 0.984375\n",
+ "with 0.1 accuracy: 0.9375\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ " \n",
+ "def f(x):\n",
+ " return x*x - 1\n",
+ "\n",
+ "print(spy.optimize.bisect(f, 0, 4))\n",
+ "\n",
+ "print('only 8 bisections: ', spy.optimize.bisect(f, 0, 4, maxiter=8))\n",
+ "\n",
+ "print('with 0.1 accuracy: ', spy.optimize.bisect(f, 0, 4, xtol=0.1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Performance\n",
+ "\n",
+ "Since the `bisect` routine calls user-defined `python` functions, the speed gain is only about a factor of two, if compared to a purely `python` implementation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:08:24.750562Z",
+ "start_time": "2020-05-19T19:08:24.682959Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "bisect running in python\r\n",
+ "execution time: 1270 us\r\n",
+ "bisect running in C\r\n",
+ "execution time: 642 us\r\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "def f(x):\n",
+ " return (x-1)*(x-1) - 2.0\n",
+ "\n",
+ "def bisect(f, a, b, xtol=2.4e-7, maxiter=100):\n",
+ " if f(a) * f(b) > 0:\n",
+ " raise ValueError\n",
+ "\n",
+ " rtb = a if f(a) < 0.0 else b\n",
+ " dx = b - a if f(a) < 0.0 else a - b\n",
+ " for i in range(maxiter):\n",
+ " dx *= 0.5\n",
+ " x_mid = rtb + dx\n",
+ " mid_value = f(x_mid)\n",
+ " if mid_value < 0:\n",
+ " rtb = x_mid\n",
+ " if abs(dx) < xtol:\n",
+ " break\n",
+ "\n",
+ " return rtb\n",
+ "\n",
+ "@timeit\n",
+ "def bisect_scipy(f, a, b):\n",
+ " return spy.optimize.bisect(f, a, b)\n",
+ "\n",
+ "@timeit\n",
+ "def bisect_timed(f, a, b):\n",
+ " return bisect(f, a, b)\n",
+ "\n",
+ "print('bisect running in python')\n",
+ "bisect_timed(f, 3, 2)\n",
+ "\n",
+ "print('bisect running in C')\n",
+ "bisect_scipy(f, 3, 2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## fmin\n",
+ "\n",
+ "`scipy`: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin.html\n",
+ "\n",
+ "The `fmin` function finds the position of the minimum of a user-defined function by using the downhill simplex method. Requires two positional arguments, the function, and the initial value. Three keyword arguments, `xatol`, `fatol`, and `maxiter` stipulate conditions for stopping."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:00:26.729947Z",
+ "start_time": "2021-01-08T13:00:26.702748Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.9996093749999952\n",
+ "1.199999999999996\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "def f(x):\n",
+ " return (x-1)**2 - 1\n",
+ "\n",
+ "print(spy.optimize.fmin(f, 3.0))\n",
+ "print(spy.optimize.fmin(f, 3.0, xatol=0.1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## trapz\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.trapz.html\n",
+ "\n",
+ "The function takes one or two one-dimensional `ndarray`s, and integrates the dependent values (`y`) using the trapezoidal rule. If the independent variable (`x`) is given, that is taken as the sample points corresponding to `y`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:01:29.515166Z",
+ "start_time": "2021-01-08T13:01:29.494285Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "x: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], dtype=float64)\n",
+ "y: array([0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0], dtype=float64)\n",
+ "============================\n",
+ "integral of y: 244.5\n",
+ "integral of y at x: 244.5\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.linspace(0, 9, num=10)\n",
+ "y = x*x\n",
+ "\n",
+ "print('x: ', x)\n",
+ "print('y: ', y)\n",
+ "print('============================')\n",
+ "print('integral of y: ', np.trapz(y))\n",
+ "print('integral of y at x: ', np.trapz(y, x=x))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-change-log.md b/circuitpython/extmod/ulab/docs/ulab-change-log.md
new file mode 100644
index 0000000..be9dc5e
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-change-log.md
@@ -0,0 +1,957 @@
+Fri, 3 Dec 2021
+
+version 3.3.8
+
+ fix any/all function
+
+Tue, 30 Nov 2021
+
+version 3.3.7
+
+ fix sum() for integer/Boolean types
+
+Sat, 20 Nov 2021
+
+version 3.3.6
+
+ fix .shape for arrays of zero length (#454)
+
+Sun, 07 Nov 2021
+
+version 3.3.5
+
+ fix cast in numpy/compare.c:compare_function()
+
+Sat, 07 Aug 2021
+
+version 3.3.4
+
+ change default keyword value in linalg.qr
+
+Fri, 23 Jul 2021
+
+version 3.3.3
+
+ fix compilation for one dimension
+
+Thu, 22 Jul 2021
+
+version 3.3.2
+
+ fix compilation error on SAMD devices
+
+Thu, 22 Jul 2021
+
+version 3.3.1
+
+ fix sum for 4D arrays
+
+Thu, 22 Jul 2021
+
+version 3.3.0
+
+ add QR decomposition
+
+Tue, 13 Jul 2021
+
+version 3.2.0
+
+ add flatiter/flat to ndarray methods
+
+Tue, 22 Jun 2021
+
+version 3.1.1
+
+ fix float comparison in scipy/linalg.c
+
+Sat, 19 Jun 2021
+
+version 3.1.0
+
+ ndarray.shape can now be assigned to
+
+Thu, 17 Jun 2021
+
+version 3.0.1
+
+ add the .T ndarray property
+
+Wed, 9 Jun 2021
+
+version 3.0.0
+
+ implement property getter/setter for micropython
+
+Thu, 3 Jun 2021
+
+version 2.9.0
+
+ add empty as alias for zeros
+
+Thu, 3 Jun 2021
+
+version 2.8.8
+
+ allow functions in approx to take iterables as argument
+
+Thu, 3 Jun 2021
+
+version 2.8.7
+
+ simplify vectorised function code
+
+Wed, 2 Jun 2021
+
+version 2.8.6
+
+ factor out array creation from iterables, so that generic iterables can be passed to numerical functions
+
+Tue, 1 Jun 2021
+
+version 2.8.5
+
+ fix upcasting rules for ndarray + scalar
+
+Mon, 31 May 2021
+
+version 2.8.4
+
+ initialise arange values via macro
+
+Mon, 24 May 2021
+
+version 2.8.3
+
+ fix nan return value
+
+Sat, 22 May 2021
+
+version 2.8.2
+
+ fix all/any/median for empty arrays
+
+Tue, 18 May 2021
+
+version 2.8.1
+
+ fix array initialisation/print with empty iterables
+
+Sun, 16 May 2021
+
+version 2.8.0
+
+ added cho_solve function in scipy.linalg module
+
+Thu, 13 May 2021
+
+version 2.7.1
+
+ fix garbage collection problem
+
+Wed, 5 May 2021
+
+version 2.7.0
+
+ added linalg module in scipy with solve_triangular function
+
+Mon, 26 Apr 2021
+
+version 2.6.2
+
+ fix optimize zero condition
+
+Sat, 23 Apr 2021
+
+version 2.6.1
+
+ fix implementation of math constants
+
+
+Mon, 22 Mar 2021
+
+version 2.6.0
+
+ add where function
+
+Mon, 8 Mar 2021
+
+version 2.5.1
+
+ fix linspace/logspace/arange for Boolean dtypes
+
+Wed, 03 Mar 2021
+
+version 2.5.0
+
+ added utils sub-module with from_intbuffer function
+
+Tue, 23 Feb 2021
+
+version 2.4.5
+
+ fix dot function
+
+Sun, 21 Feb 2021
+
+version 2.4.3
+
+ re-introduce ndarray_get_buffer, and buffer protocol
+
+Sun, 21 Feb 2021
+
+version 2.4.2
+
+ fix ndarray_is_dense, eye, ones, full, and zeros for Boolean type
+
+Sat, 13 Feb 2021
+
+version 2.4.1
+
+ fixed dot error
+
+Fri, 12 Feb 2021
+
+version 2.4.0
+
+ added byteswap method
+
+Sun, 14 Feb 2021
+
+version 2.3.7
+
+ fixed frombuffer implementation glitch
+
+Sat, 13 Feb 2021
+
+version 2.3.6
+
+ moved trace and dot to the top level
+
+Wed, 10 Feb 2021
+
+version 2.3.5
+
+ fixed invisible error in tools_reduce_axes, simplified the implementation of all/any
+
+Tue, 9 Feb 2021
+
+version 2.3.4
+
+ removed redundant exception from linalg.norm, fixed exception message in tools_reduce_axes
+
+Tue, 9 Feb 2021
+
+version 2.3.3
+
+ linalg.norm should now work with the axis keyword argument
+
+Mon, 8 Feb 2021
+
+version 2.3.2
+
+ improved the accuracy of linalg.norm, and extended it to generic iterables
+
+Mon, 8 Feb 2021
+
+version 2.3.1
+
+ partially fix https://github.com/v923z/micropython-ulab/issues/304, and len unary operator
+
+Mon, 8 Feb 2021
+
+version 2.3.0
+
+ added any and all functions
+
+Fri, 29 Jan 2021
+
+version 2.2.0
+
+ added isinf/infinite functions
+
+Fri, 29 Jan 2021
+
+version 2.1.5
+
+ fixed error, when calculating standard deviation of iterables
+
+wed, 27 Jan 2021
+
+version 2.1.4
+
+ arrays can now be initialised from nested iterables
+
+Thu, 21 Jan 2021
+
+version 2.1.3
+
+ added ifndef/endif wrappers in ulab.h
+
+Fri, 15 Jan 2021
+
+version 2.1.2
+
+ fixed small error in frombuffer
+
+Thu, 14 Jan 2021
+
+version 2.1.1
+
+ fixed bad error in diff
+
+Thu, 26 Nov 2020
+
+version 2.1.0
+
+ implemented frombuffer
+
+Tue, 24 Nov 2020
+
+version 2.0.0
+
+ implemented numpy/scipy compatibility
+
+Tue, 24 Nov 2020
+
+version 1.6.0
+
+ added Boolean initialisation option
+
+Mon, 23 Nov 2020
+
+version 1.5.1
+
+ fixed nan definition
+
+version 1.5.0
+
+ added nan/inf class level constants
+
+version 1.4.10
+
+ fixed sosfilt
+
+version 1.4.9
+
+ added in-place sort
+
+version 1.4.8
+
+ fixed convolve
+
+version 1.4.7.
+
+ fixed iteration loop in norm
+
+Fri, 20 Nov 2020
+
+version 1.4.6
+
+ fixed interp
+
+Thu, 19 Nov 2020
+
+version 1.4.5
+
+ eliminated fatal micropython error in ndarray_init_helper
+
+version 1.4.4
+
+ fixed min, max
+
+version 1.4.3
+
+ fixed full, zeros, ones
+
+version 1.4.2
+
+ fixed dtype
+
+Wed, 18 Nov 2020
+
+version 1.4.1.
+
+ fixed std
+
+version 1.4.0
+
+ removed size from linalg
+
+version 1.3.8
+
+ fixed trapz
+
+Tue, 17 Nov 2020
+
+version 1.3.7
+
+ fixed in-place power, in-place divide, roll
+
+Mon, 16 Nov 2020
+
+version 1.3.6
+
+ fixed eye
+
+Mon, 16 Nov 2020
+
+version 1.3.5
+
+ fixed trace
+
+Mon, 16 Nov 2020
+
+version 1.3.4
+
+ fixed clip
+
+Mon, 16 Nov 2020
+
+version 1.3.3
+
+ added function pointer option to some binary operators
+
+Fri, 13 Nov 2020
+
+version 1.3.2
+
+ implemented function pointer option in vectorise
+
+Thu, 12 Nov 2020
+
+version 1.3.1
+
+ factored out some of the math functions in re-usable form
+
+Wed, 11 Nov 2020
+
+version 1.3.0
+
+ added dtype function/method/property
+
+Wed, 11 Nov 2020
+
+version 1.2.8
+
+ improved the accuracy of sum for float types
+
+Wed, 11 Nov 2020
+
+version 1.2.7
+
+ fixed transpose
+ improved the accuracy of trapz
+
+Tue, 10 Nov 2020
+
+version 1.2.6
+
+ fixed slicing
+
+Mon, 9 Nov 2020
+
+version 1.2.5
+
+ fixed array casting glitch in make_new_core
+
+Mon, 9 Nov 2020
+
+version 1.2.4
+
+ sum/mean/std can flatten the arrays now
+
+Tue, 3 Nov 2020
+
+version 1.2.1
+
+ fixed pointer issue in eig, and corrected the docs
+
+Tue, 3 Nov 2020
+
+version 1.2.0
+
+ added median function
+
+Tue, 3 Nov 2020
+
+version 1.1.4
+
+ fixed norm and shape
+
+Mon, 2 Nov 2020
+
+version 1.1.3
+
+ fixed small glitch in diagonal, and ndarray_make_new_core
+
+Sun, 1 Nov 2020
+
+version 1.1.1
+
+ fixed compilation error for 4D
+
+Sat, 31 Oct 2020
+
+version 1.1.0
+
+ added the diagonal function
+
+Fri, 30 Oct 2020
+
+version 1.0.0
+
+ added :
+ support for tensors of rank 4
+ proper broadcasting
+ views
+ .tobytes()
+ concatenate
+ cross
+ full
+ logspace
+ in-place operators
+
+Sat, 25 Oct 2020
+
+version 0.54.5
+
+ wrong type in slices raise TypeError exception
+
+Fri, 23 Oct 2020
+
+version 0.54.4
+
+ fixed indexing error in slices
+
+Mon, 17 Aug 2020
+
+version 0.54.3
+
+ fixed small error in linalg
+
+Mon, 03 Aug 2020
+
+version 0.54.2
+
+ argsort throws an error, if the array is longer than 65535
+
+Wed, 29 Jul 2020
+
+version 0.54.1
+
+ changed to size_t for the length of arrays
+
+Thu, 23 Jul 2020
+
+version 0.54.0
+
+ added norm to linalg
+
+Wed, 22 Jul 2020
+
+version 0.53.2
+
+ added circuitpython documentation stubs to the source files
+
+Wed, 22 Jul 2020
+
+version 0.53.1
+
+ fixed arange with negative steps
+
+Mon, 20 Jul 2020
+
+version 0.53.0
+
+ added arange to create.c
+
+Thu, 16 Jul 2020
+
+version 0.52.0
+
+ added trapz to approx
+
+Mon, 29 Jun 2020
+
+version 0.51.1
+
+ fixed argmin/argmax issue
+
+Fri, 19 Jun 2020
+
+version 0.51.0
+
+ add sosfilt to the filter sub-module
+
+Fri, 12 Jun 2020
+
+version 0.50.2
+
+ fixes compilation error in openmv
+
+Mon, 1 Jun 2020
+
+version 0.50.1
+
+ fixes error in numerical max/min
+
+Mon, 18 May 2020
+
+version 0.50.0
+
+ move interp to the approx sub-module
+
+Wed, 06 May 2020
+
+version 0.46.0
+
+ add curve_fit to the approx sub-module
+
+version 0.44.0
+
+ add approx sub-module with newton, fmin, and bisect functions
+
+Thu, 30 Apr 2020
+
+version 0.44.0
+
+ add approx sub-module with newton, fmin, and bisect functions
+
+Tue, 19 May 2020
+
+version 0.46.1
+
+ fixed bad error in binary_op
+
+Wed, 6 May 2020
+
+version 0.46
+
+ added vectorisation of python functions
+
+Sat, 2 May 2020
+
+version 0.45.0
+
+ add equal/not_equal to the compare module
+
+Tue, 21 Apr 2020
+
+version 0.42.0
+
+ add minimum/maximum/clip functions
+
+Mon, 20 Apr 2020
+
+version 0.41.6
+
+ argument handling improvement in polyfit
+
+Mon, 20 Apr 2020
+
+version 0.41.5
+
+ fix compilation errors due to https://github.com/micropython/micropython/commit/30840ebc9925bb8ef025dbc2d5982b1bfeb75f1b
+
+Sat, 18 Apr 2020
+
+version 0.41.4
+
+ fix compilation error on hardware ports
+
+Tue, 14 Apr 2020
+
+version 0.41.3
+
+ fix indexing error in dot function
+
+Thu, 9 Apr 2020
+
+version 0.41.2
+
+ fix transpose function
+
+Tue, 7 Apr 2020
+
+version 0.41.2
+
+ fix discrepancy in argmin/argmax behaviour
+
+Tue, 7 Apr 2020
+
+version 0.41.1
+
+ fix error in argsort
+
+Sat, 4 Apr 2020
+
+version 0.41.0
+
+ implemented == and != binary operators
+
+Fri, 3 Apr 2020
+
+version 0.40.0
+
+ added trace to linalg
+
+Thu, 2 Apr 2020
+
+version 0.39.0
+
+ added the ** operator, and operand swapping in binary operators
+
+Thu, 2 Apr 2020
+
+version 0.38.1
+
+ added fast option, when initialising from ndarray_properties
+
+Thu, 12 Mar 2020
+
+version 0.38.0
+
+ added initialisation from ndarray, and the around function
+
+Tue, 10 Mar 2020
+
+version 0.37.0
+
+ added Cholesky decomposition to linalg.c
+
+Thu, 27 Feb 2020
+
+version 0.36.0
+
+ moved zeros, ones, eye and linspace into separate module (they are still bound at the top level)
+
+Thu, 27 Feb 2020
+
+version 0.35.0
+
+ Move zeros, ones back into top level ulab module
+
+Tue, 18 Feb 2020
+
+version 0.34.0
+
+ split ulab into multiple modules
+
+Sun, 16 Feb 2020
+
+version 0.33.2
+
+ moved properties into ndarray_properties.h, implemented pointer arithmetic in fft.c to save some time
+
+Fri, 14 Feb 2020
+
+version 0.33.1
+
+ added the __name__attribute to all sub-modules
+
+Thu, 13 Feb 2020
+
+version 0.33.0
+
+ sub-modules are now proper sub-modules of ulab
+
+Mon, 17 Feb 2020
+
+version 0.32.1
+
+ temporary fix for issue #40
+
+Tue, 11 Feb 2020
+
+version 0.32.0
+
+ added itemsize, size and shape attributes to ndarrays, and removed rawsize
+
+Mon, 10 Feb 2020
+
+version 0.31.0
+
+ removed asbytearray, and added buffer protocol to ndarrays, fixed bad error in filter.c
+
+Sun, 09 Feb 2020
+
+version 0.30.2
+
+ fixed slice_length in ndarray.c
+
+Sat, 08 Feb 2020
+
+version 0.30.1
+
+ fixed typecode error, added variable inspection, and replaced ternary operators in filter.c
+
+Fri, 07 Feb 2020
+
+version 0.30.0
+
+ ulab functions can arbitrarily be excluded from the firmware via the ulab.h configuration file
+
+Thu, 06 Feb 2020
+
+version 0.27.0
+
+ add convolve, the start of a 'filter' functionality group
+
+Wed, 29 Jan 2020
+
+version 0.26.7
+
+ fixed indexing error in linalg.dot
+
+Mon, 20 Jan 2020
+
+version 0.26.6
+
+ replaced MP_ROM_PTR(&mp_const_none_obj), so that module can be compiled for the nucleo board
+
+Tue, 7 Jan 2020
+
+version 0.26.5
+
+ fixed glitch in numerical.c, numerical.h
+
+Mon, 6 Jan 2020
+
+version 0.26.4
+
+ switched version constant to string
+
+Tue, 31 Dec 2019
+
+version 0.263
+
+ changed declaration of ulab_ndarray_type to extern
+
+Fri, 29 Nov 2019
+
+version 0.262
+
+ fixed error in macro in vectorise.h
+
+Thu, 28 Nov 2019
+
+version 0.261
+
+ fixed bad indexing error in linalg.dot
+
+Tue, 6 Nov 2019
+
+version 0.26
+
+ added in-place sorting (method of ndarray), and argsort
+
+Mon, 4 Nov 2019
+
+version 0.25
+
+ added first implementation of sort, and fixed section on compiling the module in the manual
+
+Thu, 31 Oct 2019
+
+version 0.24
+
+ added diff to numerical.c
+
+Tue, 29 Oct 2019
+
+version 0.23
+
+ major revamp of subscription method
+
+Sat, 19 Oct 2019
+
+version 0.21
+
+ fixed trivial bug in .rawsize()
+
+Sat, 19 Oct 2019
+
+version 0.22
+
+ fixed small error in linalg_det, and implemented linalg_eig.
+
+
+Thu, 17 Oct 2019
+
+version 0.21
+
+ implemented uniform interface for fft, and spectrum, and added ifft.
+
+Wed, 16 Oct 2019
+
+version 0.20
+
+ Added flip function to numerical.c, and moved the size function to linalg. In addition,
+ size is a function now, and not a method.
+
+Tue, 15 Oct 2019
+
+version 0.19
+
+ fixed roll in numerical.c: it can now accept the axis=None keyword argument, added determinant to linalg.c
+
+Mon, 14 Oct 2019
+
+version 0.18
+
+ fixed min/man function in numerical.c; it conforms to numpy behaviour
+
+Fri, 11 Oct 2019
+
+version 0.171
+
+ found and fixed small bux in roll function
+
+Fri, 11 Oct 2019
+
+version 0.17
+
+ universal function can now take arbitrary typecodes
+
+Fri, 11 Oct 2019
+
+version 0.161
+
+ fixed bad error in iterator, and make_new_ndarray
+
+Thu, 10 Oct 2019
+
+varsion 0.16
+
+ changed ndarray to array in ulab.c, so as to conform to numpy's notation
+ extended subscr method to include slices (partially works)
+
+Tue, 8 Oct 2019
+
+version 0.15
+
+ added inv, neg, pos, and abs unary operators to ndarray.c
+
+Mon, 7 Oct 2019
+
+version 0.14
+
+ made the internal binary_op function tighter, and added keyword arguments to linspace
+
+Sat, 4 Oct 2019
+
+version 0.13
+
+ added the <, <=, >, >= binary operators to ndarray
+
+Fri, 4 Oct 2019
+
+version 0.12
+
+ added .flatten to ndarray, ones, zeros, and eye to linalg
+
+Thu, 3 Oct 2019
+
+version 0.11
+
+ binary operators are now based on macros
diff --git a/circuitpython/extmod/ulab/docs/ulab-compare.ipynb b/circuitpython/extmod/ulab/docs/ulab-compare.ipynb
new file mode 100644
index 0000000..69fa762
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-compare.ipynb
@@ -0,0 +1,467 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:02:42.934528Z",
+ "start_time": "2021-01-08T13:02:42.720862Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:02:44.890094Z",
+ "start_time": "2021-01-08T13:02:44.878787Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:06:20.583308Z",
+ "start_time": "2021-01-08T13:06:20.525830Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Comparison of arrays"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## equal, not_equal\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.equal.html\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.not_equal.html\n",
+ "\n",
+ "In `micropython`, equality of arrays or scalars can be established by utilising the `==`, `!=`, `<`, `>`, `<=`, or `=>` binary operators. In `circuitpython`, `==` and `!=` will produce unexpected results. In order to avoid this discrepancy, and to maintain compatibility with `numpy`, `ulab` implements the `equal` and `not_equal` operators that return the same results, irrespective of the `python` implementation.\n",
+ "\n",
+ "These two functions take two `ndarray`s, or scalars as their arguments. No keyword arguments are implemented."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T14:22:13.990898Z",
+ "start_time": "2021-01-08T14:22:13.941896Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "b: array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=float64)\n",
+ "\n",
+ "a == b: array([True, False, False, False, False, False, False, False, False], dtype=bool)\n",
+ "a != b: array([False, True, True, True, True, True, True, True, True], dtype=bool)\n",
+ "a == 2: array([False, False, True, False, False, False, False, False, False], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9))\n",
+ "b = np.zeros(9)\n",
+ "\n",
+ "print('a: ', a)\n",
+ "print('b: ', b)\n",
+ "print('\\na == b: ', np.equal(a, b))\n",
+ "print('a != b: ', np.not_equal(a, b))\n",
+ "\n",
+ "# comparison with scalars\n",
+ "print('a == 2: ', np.equal(a, 2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## minimum\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.minimum.html\n",
+ "\n",
+ "Returns the minimum of two arrays, or two scalars, or an array, and a scalar. If the arrays are of different `dtype`, the output is upcast as in [Binary operators](#Binary-operators). If both inputs are scalars, a scalar is returned. Only positional arguments are implemented.\n",
+ "\n",
+ "## maximum\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.maximum.html\n",
+ "\n",
+ "Returns the maximum of two arrays, or two scalars, or an array, and a scalar. If the arrays are of different `dtype`, the output is upcast as in [Binary operators](#Binary-operators). If both inputs are scalars, a scalar is returned. Only positional arguments are implemented."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:21:17.151280Z",
+ "start_time": "2021-01-08T13:21:17.123768Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "minimum of a, and b:\n",
+ "array([1.0, 2.0, 3.0, 2.0, 1.0], dtype=float64)\n",
+ "\n",
+ "maximum of a, and b:\n",
+ "array([5.0, 4.0, 3.0, 4.0, 5.0], dtype=float64)\n",
+ "\n",
+ "maximum of 1, and 5.5:\n",
+ "5.5\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)\n",
+ "b = np.array([5, 4, 3, 2, 1], dtype=np.float)\n",
+ "print('minimum of a, and b:')\n",
+ "print(np.minimum(a, b))\n",
+ "\n",
+ "print('\\nmaximum of a, and b:')\n",
+ "print(np.maximum(a, b))\n",
+ "\n",
+ "print('\\nmaximum of 1, and 5.5:')\n",
+ "print(np.maximum(1, 5.5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## clip\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html\n",
+ "\n",
+ "Clips an array, i.e., values that are outside of an interval are clipped to the interval edges. The function is equivalent to `maximum(a_min, minimum(a, a_max))` broadcasting takes place exactly as in [minimum](#minimum). If the arrays are of different `dtype`, the output is upcast as in [Binary operators](#Binary-operators)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T13:22:14.147310Z",
+ "start_time": "2021-01-08T13:22:14.123961Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "clipped:\t array([3, 3, 3, 3, 4, 5, 6, 7, 7], dtype=uint8)\n",
+ "\n",
+ "a:\t\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "b:\t\t array([3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=float64)\n",
+ "clipped:\t array([3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 7.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "print('a:\\t\\t', a)\n",
+ "print('clipped:\\t', np.clip(a, 3, 7))\n",
+ "\n",
+ "b = 3 * np.ones(len(a), dtype=np.float)\n",
+ "print('\\na:\\t\\t', a)\n",
+ "print('b:\\t\\t', b)\n",
+ "print('clipped:\\t', np.clip(a, b, 7))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-convert.ipynb b/circuitpython/extmod/ulab/docs/ulab-convert.ipynb
new file mode 100644
index 0000000..70c6fa4
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-convert.ipynb
@@ -0,0 +1,507 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-01T09:27:13.438054Z",
+ "start_time": "2020-05-01T09:27:13.191491Z"
+ }
+ },
+ "source": [
+ "# conf.py"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:24:12.745063Z",
+ "start_time": "2022-01-07T18:24:12.733067Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Overwriting manual/source/conf.py\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%writefile manual/source/conf.py\n",
+ "# Configuration file for the Sphinx documentation builder.\n",
+ "#\n",
+ "# This file only contains a selection of the most common options. For a full\n",
+ "# list see the documentation:\n",
+ "# http://www.sphinx-doc.org/en/master/config\n",
+ "\n",
+ "# -- Path setup --------------------------------------------------------------\n",
+ "\n",
+ "# If extensions (or modules to document with autodoc) are in another directory,\n",
+ "# add these directories to sys.path here. If the directory is relative to the\n",
+ "# documentation root, use os.path.abspath to make it absolute, like shown here.\n",
+ "#\n",
+ "import os\n",
+ "# import sys\n",
+ "# sys.path.insert(0, os.path.abspath('.'))\n",
+ "\n",
+ "#import sphinx_rtd_theme\n",
+ "\n",
+ "from sphinx.transforms import SphinxTransform\n",
+ "from docutils import nodes\n",
+ "from sphinx import addnodes\n",
+ "\n",
+ "# -- Project information -----------------------------------------------------\n",
+ "\n",
+ "project = 'The ulab book'\n",
+ "copyright = '2019-2022, Zoltán Vörös and contributors'\n",
+ "author = 'Zoltán Vörös'\n",
+ "\n",
+ "# The full version, including alpha/beta/rc tags\n",
+ "release = '4.0.0'\n",
+ "\n",
+ "\n",
+ "# -- General configuration ---------------------------------------------------\n",
+ "\n",
+ "# Add any Sphinx extension module names here, as strings. They can be\n",
+ "# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n",
+ "# ones.\n",
+ "extensions = [\n",
+ "]\n",
+ "\n",
+ "# Add any paths that contain templates here, relative to this directory.\n",
+ "templates_path = ['_templates']\n",
+ "\n",
+ "# List of patterns, relative to source directory, that match files and\n",
+ "# directories to ignore when looking for source files.\n",
+ "# This pattern also affects html_static_path and html_extra_path.\n",
+ "exclude_patterns = []\n",
+ "\n",
+ "\n",
+ "# Add any paths that contain custom static files (such as style sheets) here,\n",
+ "# relative to this directory. They are copied after the builtin static files,\n",
+ "# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n",
+ "html_static_path = ['_static']\n",
+ "\n",
+ "latex_maketitle = r'''\n",
+ "\\begin{titlepage}\n",
+ "\\begin{flushright}\n",
+ "\\Huge\\textbf{The $\\mu$lab book}\n",
+ "\\vskip 0.5em\n",
+ "\\LARGE\n",
+ "\\textbf{Release %s}\n",
+ "\\vskip 5em\n",
+ "\\huge\\textbf{Zoltán Vörös}\n",
+ "\\end{flushright}\n",
+ "\\begin{flushright}\n",
+ "\\LARGE\n",
+ "\\vskip 2em\n",
+ "with contributions by\n",
+ "\\vskip 2em\n",
+ "\\textbf{Roberto Colistete Jr.}\n",
+ "\\vskip 0.2em\n",
+ "\\textbf{Jeff Epler}\n",
+ "\\vskip 0.2em\n",
+ "\\textbf{Taku Fukada}\n",
+ "\\vskip 0.2em\n",
+ "\\textbf{Diego Elio Pettenò}\n",
+ "\\vskip 0.2em\n",
+ "\\textbf{Scott Shawcroft}\n",
+ "\\vskip 5em\n",
+ "\\today\n",
+ "\\end{flushright}\n",
+ "\\end{titlepage}\n",
+ "'''%release\n",
+ "\n",
+ "latex_elements = {\n",
+ " 'maketitle': latex_maketitle\n",
+ "}\n",
+ "\n",
+ "\n",
+ "master_doc = 'index'\n",
+ "\n",
+ "author=u'Zoltán Vörös'\n",
+ "copyright=author\n",
+ "language='en'\n",
+ "\n",
+ "latex_documents = [\n",
+ "(master_doc, 'the-ulab-book.tex', 'The $\\mu$lab book',\n",
+ "'Zoltán Vörös', 'manual'),\n",
+ "]\n",
+ "\n",
+ "# Read the docs theme\n",
+ "on_rtd = os.environ.get('READTHEDOCS', None) == 'True'\n",
+ "if not on_rtd:\n",
+ " try:\n",
+ " import sphinx_rtd_theme\n",
+ " html_theme = 'sphinx_rtd_theme'\n",
+ " html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.']\n",
+ " except ImportError:\n",
+ " html_theme = 'default'\n",
+ " html_theme_path = ['.']\n",
+ "else:\n",
+ " html_theme_path = ['.']"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-05-09T06:06:28.491158Z",
+ "start_time": "2021-05-09T06:06:28.477127Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Overwriting manual/source/index.rst\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%writefile manual/source/index.rst\n",
+ "\n",
+ ".. ulab-manual documentation master file, created by\n",
+ " sphinx-quickstart on Sat Oct 19 12:48:00 2019.\n",
+ " You can adapt this file completely to your liking, but it should at least\n",
+ " contain the root `toctree` directive.\n",
+ "\n",
+ "Welcome to the ulab book!\n",
+ "=======================================\n",
+ "\n",
+ ".. toctree::\n",
+ " :maxdepth: 2\n",
+ " :caption: Introduction\n",
+ "\n",
+ " ulab-intro\n",
+ "\n",
+ ".. toctree::\n",
+ " :maxdepth: 2\n",
+ " :caption: User's guide:\n",
+ "\n",
+ " ulab-ndarray\n",
+ " numpy-functions\n",
+ " numpy-universal\n",
+ " numpy-fft\n",
+ " numpy-linalg\n",
+ " scipy-linalg\n",
+ " scipy-optimize\n",
+ " scipy-signal\n",
+ " scipy-special\n",
+ " ulab-utils\n",
+ " ulab-tricks\n",
+ " ulab-programming\n",
+ "\n",
+ "Indices and tables\n",
+ "==================\n",
+ "\n",
+ "* :ref:`genindex`\n",
+ "* :ref:`modindex`\n",
+ "* :ref:`search`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Notebook conversion"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:24:27.671415Z",
+ "start_time": "2022-01-07T18:24:24.933205Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import nbformat as nb\n",
+ "import nbformat.v4.nbbase as nb4\n",
+ "from nbconvert import RSTExporter\n",
+ "\n",
+ "from jinja2 import FileSystemLoader\n",
+ "rstexporter = RSTExporter(\n",
+ " extra_loaders=[FileSystemLoader('./templates')],\n",
+ " template_file = './templates/manual.tpl'\n",
+ ")\n",
+ "\n",
+ "def convert_notebook(fn):\n",
+ " source = nb.read(fn+'.ipynb', nb.NO_CONVERT)\n",
+ " notebook = nb4.new_notebook()\n",
+ " notebook.cells = []\n",
+ " append_cell = False\n",
+ " for cell in source['cells']:\n",
+ " if append_cell:\n",
+ " notebook.cells.append(cell)\n",
+ " else:\n",
+ " if cell.cell_type == 'markdown':\n",
+ " if cell.source == '__END_OF_DEFS__':\n",
+ " append_cell = True\n",
+ " \n",
+ " (rst, resources) = rstexporter.from_notebook_node(notebook)\n",
+ " with open('./manual/source/' + fn + '.rst', 'w') as fout:\n",
+ " # it's a bit odd, but even an emtpy notebook is converted into a \"None\" string\n",
+ " rst = rst.lstrip('None')\n",
+ " fout.write(rst)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:52:29.910335Z",
+ "start_time": "2022-01-07T19:52:28.432391Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "files = ['ulab-intro',\n",
+ " 'ulab-ndarray',\n",
+ " 'numpy-functions', \n",
+ " 'numpy-universal',\n",
+ " 'numpy-fft',\n",
+ " 'numpy-linalg',\n",
+ " 'scipy-linalg',\n",
+ " 'scipy-optimize',\n",
+ " 'scipy-signal',\n",
+ " 'scipy-special',\n",
+ " 'ulab-utils',\n",
+ " 'ulab-tricks',\n",
+ " 'ulab-programming']\n",
+ "\n",
+ "for file in files:\n",
+ " convert_notebook(file)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Template"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-30T19:04:50.295563Z",
+ "start_time": "2020-10-30T19:04:50.227535Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Overwriting ./templates/manual.tpl\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%writefile ./templates/manual.tpl\n",
+ "\n",
+ "{%- extends 'display_priority.tpl' -%}\n",
+ "\n",
+ "\n",
+ "{% block in_prompt %}\n",
+ "{% endblock in_prompt %}\n",
+ "\n",
+ "{% block output_prompt %}\n",
+ "{% endblock output_prompt %}\n",
+ "\n",
+ "{% block input scoped%}\n",
+ "\n",
+ "{%- if cell.source.split('\\n')[0].startswith('%%micropython') -%}\n",
+ ".. code::\n",
+ " \n",
+ "{{ '\\n'.join(['# code to be run in micropython'] + cell.source.strip().split('\\n')[1:]) | indent}}\n",
+ "\n",
+ "{%- else -%}\n",
+ ".. code::\n",
+ "\n",
+ "{{ '\\n'.join(['# code to be run in CPython\\n'] + cell.source.strip().split('\\n')) | indent}}\n",
+ "{%- endif -%}\n",
+ "{% endblock input %}\n",
+ "\n",
+ "{% block error %}\n",
+ "::\n",
+ "\n",
+ "{{ super() }}\n",
+ "{% endblock error %}\n",
+ "\n",
+ "{% block traceback_line %}\n",
+ "{{ line | indent | strip_ansi }}\n",
+ "{% endblock traceback_line %}\n",
+ "\n",
+ "{% block execute_result %}\n",
+ "{% block data_priority scoped %}\n",
+ "{{ super() }}\n",
+ "{% endblock %}\n",
+ "{% endblock execute_result %}\n",
+ "\n",
+ "{% block stream %}\n",
+ ".. parsed-literal::\n",
+ "\n",
+ "{{ output.text | indent }}\n",
+ "{% endblock stream %}\n",
+ "\n",
+ "{% block data_svg %}\n",
+ ".. image:: {{ output.metadata.filenames['image/svg+xml'] | urlencode }}\n",
+ "{% endblock data_svg %}\n",
+ "\n",
+ "{% block data_png %}\n",
+ ".. image:: {{ output.metadata.filenames['image/png'] | urlencode }}\n",
+ "{%- set width=output | get_metadata('width', 'image/png') -%}\n",
+ "{%- if width is not none %}\n",
+ " :width: {{ width }}px\n",
+ "{%- endif %}\n",
+ "{%- set height=output | get_metadata('height', 'image/png') -%}\n",
+ "{%- if height is not none %}\n",
+ " :height: {{ height }}px\n",
+ "{%- endif %}\n",
+ "{% endblock data_png %}\n",
+ "\n",
+ "{% block data_jpg %}\n",
+ ".. image:: {{ output.metadata.filenames['image/jpeg'] | urlencode }}\n",
+ "{%- set width=output | get_metadata('width', 'image/jpeg') -%}\n",
+ "{%- if width is not none %}\n",
+ " :width: {{ width }}px\n",
+ "{%- endif %}\n",
+ "{%- set height=output | get_metadata('height', 'image/jpeg') -%}\n",
+ "{%- if height is not none %}\n",
+ " :height: {{ height }}px\n",
+ "{%- endif %}\n",
+ "{% endblock data_jpg %}\n",
+ "\n",
+ "{% block data_markdown %}\n",
+ "{{ output.data['text/markdown'] | convert_pandoc(\"markdown\", \"rst\") }}\n",
+ "{% endblock data_markdown %}\n",
+ "\n",
+ "{% block data_latex %}\n",
+ ".. math::\n",
+ "\n",
+ "{{ output.data['text/latex'] | strip_dollars | indent }}\n",
+ "{% endblock data_latex %}\n",
+ "\n",
+ "{% block data_text scoped %}\n",
+ ".. parsed-literal::\n",
+ "\n",
+ "{{ output.data['text/plain'] | indent }}\n",
+ "{% endblock data_text %}\n",
+ "\n",
+ "{% block data_html scoped %}\n",
+ ".. raw:: html\n",
+ "\n",
+ "{{ output.data['text/html'] | indent }}\n",
+ "{% endblock data_html %}\n",
+ "\n",
+ "{% block markdowncell scoped %}\n",
+ "{{ cell.source | convert_pandoc(\"markdown\", \"rst\") }}\n",
+ "{% endblock markdowncell %}\n",
+ "\n",
+ "{%- block rawcell scoped -%}\n",
+ "{%- if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) %}\n",
+ "{{cell.source}}\n",
+ "{% endif -%}\n",
+ "{%- endblock rawcell -%}\n",
+ "\n",
+ "{% block headingcell scoped %}\n",
+ "{{ (\"#\" * cell.level + cell.source) | replace('\\n', ' ') | convert_pandoc(\"markdown\", \"rst\") }}\n",
+ "{% endblock headingcell %}\n",
+ "\n",
+ "{% block unknowncell scoped %}\n",
+ "unknown type {{cell.type}}\n",
+ "{% endblock unknowncell %}\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "interpreter": {
+ "hash": "ce9a02f9f7db620716422019cafa4bc1786ca85daa298b819f6da075e7993842"
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-intro.ipynb b/circuitpython/extmod/ulab/docs/ulab-intro.ipynb
new file mode 100644
index 0000000..67d6b60
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-intro.ipynb
@@ -0,0 +1,897 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:07:55.382930Z",
+ "start_time": "2021-01-08T12:07:46.895325Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Matplotlib is building the font cache; this may take a moment.\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:13:14.590799Z",
+ "start_time": "2022-01-07T18:13:14.585845Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:20:56.550047Z",
+ "start_time": "2022-01-07T18:20:56.527475Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Introduction"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Enter ulab\n",
+ "\n",
+ "`ulab` is a `numpy`-like module for `micropython` and its derivatives, meant to simplify and speed up common mathematical operations on arrays. `ulab` implements a small subset of `numpy` and `scipy`. The functions were chosen such that they might be useful in the context of a microcontroller. However, the project is a living one, and suggestions for new features are always welcome. \n",
+ "\n",
+ "This document discusses how you can use the library, starting from building your own firmware, through questions like what affects the firmware size, what are the trade-offs, and what are the most important differences to `numpy` and `scipy`, respectively. The document is organised as follows:\n",
+ "\n",
+ "The chapter after this one helps you with firmware customisation.\n",
+ "\n",
+ "The third chapter gives a very concise summary of the `ulab` functions and array methods. This chapter can be used as a quick reference.\n",
+ "\n",
+ "The chapters after that are an in-depth review of most functions. Here you can find usage examples, benchmarks, as well as a thorough discussion of such concepts as broadcasting, and views versus copies. \n",
+ "\n",
+ "The final chapter of this book can be regarded as the programming manual. The inner working of `ulab` is dissected here, and you will also find hints as to how to implement your own `numpy`-compatible functions.\n",
+ "\n",
+ "\n",
+ "## Purpose\n",
+ "\n",
+ "Of course, the first question that one has to answer is, why on Earth one would need a fast math library on a microcontroller. After all, it is not expected that heavy number crunching is going to take place on bare metal. It is not meant to. On a PC, the main reason for writing fast code is the sheer amount of data that one wants to process. On a microcontroller, the data volume is probably small, but it might lead to catastrophic system failure, if these data are not processed in time, because the microcontroller is supposed to interact with the outside world in a timely fashion. In fact, this latter objective was the initiator of this project: I needed the Fourier transform of a signal coming from the ADC of the `pyboard`, and all available options were simply too slow. \n",
+ "\n",
+ "In addition to speed, another issue that one has to keep in mind when working with embedded systems is the amount of available RAM: I believe, everything here could be implemented in pure `python` with relatively little effort (in fact, there are a couple of `python`-only implementations of `numpy` functions out there), but the price we would have to pay for that is not only speed, but RAM, too. `python` code, if is not frozen, and compiled into the firmware, has to be compiled at runtime, which is not exactly a cheap process. On top of that, if numbers are stored in a list or tuple, which would be the high-level container, then they occupy 8 bytes, no matter, whether they are all smaller than 100, or larger than one hundred million. This is obviously a waste of resources in an environment, where resources are scarce. \n",
+ "\n",
+ "Finally, there is a reason for using `micropython` in the first place. Namely, that a microcontroller can be programmed in a very elegant, and *pythonic* way. But if it is so, why should we not extend this idea to other tasks and concepts that might come up in this context? If there was no other reason than this *elegance*, I would find that convincing enough.\n",
+ "\n",
+ "Based on the above-mentioned considerations, all functions in `ulab` are implemented in a way that \n",
+ "\n",
+ "1. conforms to `numpy` as much as possible\n",
+ "2. is so frugal with RAM as possible,\n",
+ "3. and yet, fast. Much faster than pure python. Think of speed-ups of 30-50!\n",
+ "\n",
+ "The main points of `ulab` are \n",
+ "\n",
+ "- compact, iterable and slicable containers of numerical data in one to four dimensions. These containers support all the relevant unary and binary operators (e.g., `len`, ==, +, *, etc.)\n",
+ "- vectorised computations on `micropython` iterables and numerical arrays (in `numpy`-speak, universal functions)\n",
+ "- computing statistical properties (mean, standard deviation etc.) on arrays\n",
+ "- basic linear algebra routines (matrix inversion, multiplication, reshaping, transposition, determinant, and eigenvalues, Cholesky decomposition and so on)\n",
+ "- polynomial fits to numerical data, and evaluation of polynomials\n",
+ "- fast Fourier transforms\n",
+ "- filtering of data (convolution and second-order filters)\n",
+ "- function minimisation, fitting, and numerical approximation routines\n",
+ "- interfacing between numerical data and peripheral hardware devices\n",
+ "\n",
+ "`ulab` implements close to a hundred functions and array methods. At the time of writing this manual (for version 4.0.0), the library adds approximately 120 kB of extra compiled code to the `micropython` (pyboard.v.1.17) firmware. However, if you are tight with flash space, you can easily shave tens of kB off the firmware. In fact, if only a small sub-set of functions are needed, you can get away with less than 10 kB of flash space. See the section on [customising ulab](#Customising-the-firmware).\n",
+ "\n",
+ "## Resources and legal matters\n",
+ "\n",
+ "The source code of the module can be found under https://github.com/v923z/micropython-ulab/tree/master/code. while the source of this user manual is under https://github.com/v923z/micropython-ulab/tree/master/docs.\n",
+ "\n",
+ "The MIT licence applies to all material. \n",
+ "\n",
+ "## Friendly request\n",
+ "\n",
+ "If you use `ulab`, and bump into a bug, or think that a particular function is missing, or its behaviour does not conform to `numpy`, please, raise a [ulab issue](#https://github.com/v923z/micropython-ulab/issues) on github, so that the community can profit from your experiences. \n",
+ "\n",
+ "Even better, if you find the project to be useful, and think that it could be made better, faster, tighter, and shinier, please, consider contributing, and issue a pull request with the implementation of your improvements and new features. `ulab` can only become successful, if it offers what the community needs.\n",
+ "\n",
+ "These last comments apply to the documentation, too. If, in your opinion, the documentation is obscure, misleading, or not detailed enough, please, let us know, so that *we* can fix it.\n",
+ "\n",
+ "## Differences between micropython-ulab and circuitpython-ulab\n",
+ "\n",
+ "`ulab` has originally been developed for `micropython`, but has since been integrated into a number of its flavours. Most of these are simply forks of `micropython` itself, with some additional functionality. One of the notable exceptions is `circuitpython`, which has slightly diverged at the core level, and this has some minor consequences. Some of these concern the C implementation details only, which all have been sorted out with the generous and enthusiastic support of Jeff Epler from [Adafruit Industries](http://www.adafruit.com).\n",
+ "\n",
+ "There are, however, a couple of instances, where the two environments differ at the python level in how the class properties can be accessed. We will point out the differences and possible workarounds at the relevant places in this document."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Customising the firmware\n",
+ "\n",
+ "\n",
+ "As mentioned above, `ulab` has considerably grown since its conception, which also means that it might no longer fit on the microcontroller of your choice. There are, however, a couple of ways of customising the firmware, and thereby reducing its size. \n",
+ "\n",
+ "All `ulab` options are listed in a single header file, [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h), which contains pre-processor flags for each feature that can be fine-tuned. The first couple of lines of the file look like this\n",
+ "\n",
+ "```c\n",
+ "// The pre-processor constants in this file determine how ulab behaves:\n",
+ "//\n",
+ "// - how many dimensions ulab can handle\n",
+ "// - which functions are included in the compiled firmware\n",
+ "// - whether the python syntax is numpy-like, or modular\n",
+ "// - whether arrays can be sliced and iterated over\n",
+ "// - which binary/unary operators are supported\n",
+ "//\n",
+ "// A considerable amount of flash space can be saved by removing (setting\n",
+ "// the corresponding constants to 0) the unnecessary functions and features.\n",
+ "\n",
+ "// Values defined here can be overridden by your own config file as\n",
+ "// make -DULAB_CONFIG_FILE=\"my_ulab_config.h\"\n",
+ "#if defined(ULAB_CONFIG_FILE)\n",
+ "#include ULAB_CONFIG_FILE\n",
+ "#endif\n",
+ "\n",
+ "// Adds support for complex ndarrays\n",
+ "#ifndef ULAB_SUPPORTS_COMPLEX\n",
+ "#define ULAB_SUPPORTS_COMPLEX (1)\n",
+ "#endif\n",
+ "\n",
+ "// Determines, whether scipy is defined in ulab. The sub-modules and functions\n",
+ "// of scipy have to be defined separately\n",
+ "#define ULAB_HAS_SCIPY (1)\n",
+ "\n",
+ "// The maximum number of dimensions the firmware should be able to support\n",
+ "// Possible values lie between 1, and 4, inclusive\n",
+ "#define ULAB_MAX_DIMS 2\n",
+ "\n",
+ "// By setting this constant to 1, iteration over array dimensions will be implemented\n",
+ "// as a function (ndarray_rewind_array), instead of writing out the loops in macros\n",
+ "// This reduces firmware size at the expense of speed\n",
+ "#define ULAB_HAS_FUNCTION_ITERATOR (0)\n",
+ "\n",
+ "// If NDARRAY_IS_ITERABLE is 1, the ndarray object defines its own iterator function\n",
+ "// This option saves approx. 250 bytes of flash space\n",
+ "#define NDARRAY_IS_ITERABLE (1)\n",
+ "\n",
+ "// Slicing can be switched off by setting this variable to 0\n",
+ "#define NDARRAY_IS_SLICEABLE (1)\n",
+ "\n",
+ "// The default threshold for pretty printing. These variables can be overwritten\n",
+ "// at run-time via the set_printoptions() function\n",
+ "#define ULAB_HAS_PRINTOPTIONS (1)\n",
+ "#define NDARRAY_PRINT_THRESHOLD 10\n",
+ "#define NDARRAY_PRINT_EDGEITEMS 3\n",
+ "\n",
+ "// determines, whether the dtype is an object, or simply a character\n",
+ "// the object implementation is numpythonic, but requires more space\n",
+ "#define ULAB_HAS_DTYPE_OBJECT (0)\n",
+ "\n",
+ "// the ndarray binary operators\n",
+ "#define NDARRAY_HAS_BINARY_OPS (1)\n",
+ "\n",
+ "// Firmware size can be reduced at the expense of speed by using function\n",
+ "// pointers in iterations. For each operator, he function pointer saves around\n",
+ "// 2 kB in the two-dimensional case, and around 4 kB in the four-dimensional case.\n",
+ "\n",
+ "#define NDARRAY_BINARY_USES_FUN_POINTER (0)\n",
+ "\n",
+ "#define NDARRAY_HAS_BINARY_OP_ADD (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_EQUAL (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_LESS (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_LESS_EQUAL (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_MORE (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_MORE_EQUAL (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_MULTIPLY (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_NOT_EQUAL (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_POWER (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_SUBTRACT (1)\n",
+ "#define NDARRAY_HAS_BINARY_OP_TRUE_DIVIDE (1)\n",
+ "... \n",
+ "```\n",
+ "\n",
+ "The meaning of flags with names `_HAS_` should be obvious, so we will just explain the other options. \n",
+ "\n",
+ "To see how much you can gain by un-setting the functions that you do not need, here are some pointers. In four dimensions, including all functions adds around 120 kB to the `micropython` firmware. On the other hand, if you are interested in Fourier transforms only, and strip everything else, you get away with less than 5 kB extra. \n",
+ "\n",
+ "## Compatibility with numpy\n",
+ "\n",
+ "The functions implemented in `ulab` are organised in four sub-modules at the C level, namely, `numpy`, `scipy`, `utils`, and `user`. This modularity is elevated to `python`, meaning that in order to use functions that are part of `numpy`, you have to import `numpy` as\n",
+ "\n",
+ "```python\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "x = np.array([4, 5, 6])\n",
+ "p = np.array([1, 2, 3])\n",
+ "np.polyval(p, x)\n",
+ "```\n",
+ "\n",
+ "There are a couple of exceptions to this rule, namely `fft`, and `linalg`, which are sub-modules even in `numpy`, thus you have to write them out as \n",
+ "\n",
+ "```python\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "A = np.array([1, 2, 3, 4]).reshape()\n",
+ "np.linalg.trace(A)\n",
+ "```\n",
+ "\n",
+ "Some of the functions in `ulab` are re-implementations of `scipy` functions, and they are to be imported as \n",
+ "\n",
+ "```python\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "\n",
+ "x = np.array([1, 2, 3])\n",
+ "spy.special.erf(x)\n",
+ "```\n",
+ "\n",
+ "`numpy`-compatibility has an enormous benefit : namely, by `try`ing to `import`, we can guarantee that the same, unmodified code runs in `CPython`, as in `micropython`. The following snippet is platform-independent, thus, the `python` code can be tested and debugged on a computer before loading it onto the microcontroller.\n",
+ "\n",
+ "```python\n",
+ "\n",
+ "try:\n",
+ " from ulab import numpy as np\n",
+ " from ulab import scipy as spy\n",
+ "except ImportError:\n",
+ " import numpy as np\n",
+ " import scipy as spy\n",
+ " \n",
+ "x = np.array([1, 2, 3])\n",
+ "spy.special.erf(x) \n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The impact of dimensionality\n",
+ "\n",
+ "### Reducing the number of dimensions\n",
+ "\n",
+ "`ulab` supports tensors of rank four, but this is expensive in terms of flash: with all available functions and options, the library adds around 100 kB to the firmware. However, if such high dimensions are not required, significant reductions in size can be gotten by changing the value of \n",
+ "\n",
+ "```c\n",
+ "#define ULAB_MAX_DIMS 2\n",
+ "```\n",
+ "\n",
+ "Two dimensions cost a bit more than half of four, while you can get away with around 20 kB of flash in one dimension, because all those functions that don't make sense (e.g., matrix inversion, eigenvalues etc.) are automatically stripped from the firmware.\n",
+ "\n",
+ "### Using the function iterator\n",
+ "\n",
+ "In higher dimensions, the firmware size increases, because each dimension (axis) adds another level of nested loops. An example of this is the macro of the binary operator in three dimensions\n",
+ "\n",
+ "```c\n",
+ "#define BINARY_LOOP(results, type_out, type_left, type_right, larray, lstrides, rarray, rstrides, OPERATOR)\n",
+ " type_out *array = (type_out *)results->array;\n",
+ " size_t j = 0;\n",
+ " do {\n",
+ " size_t k = 0;\n",
+ " do {\n",
+ " size_t l = 0;\n",
+ " do {\n",
+ " *array++ = *((type_left *)(larray)) OPERATOR *((type_right *)(rarray));\n",
+ " (larray) += (lstrides)[ULAB_MAX_DIMS - 1];\n",
+ " (rarray) += (rstrides)[ULAB_MAX_DIMS - 1];\n",
+ " l++;\n",
+ " } while(l < (results)->shape[ULAB_MAX_DIMS - 1]);\n",
+ " (larray) -= (lstrides)[ULAB_MAX_DIMS - 1] * (results)->shape[ULAB_MAX_DIMS-1];\n",
+ " (larray) += (lstrides)[ULAB_MAX_DIMS - 2];\n",
+ " (rarray) -= (rstrides)[ULAB_MAX_DIMS - 1] * (results)->shape[ULAB_MAX_DIMS-1];\n",
+ " (rarray) += (rstrides)[ULAB_MAX_DIMS - 2];\n",
+ " k++;\n",
+ " } while(k < (results)->shape[ULAB_MAX_DIMS - 2]);\n",
+ " (larray) -= (lstrides)[ULAB_MAX_DIMS - 2] * results->shape[ULAB_MAX_DIMS-2];\n",
+ " (larray) += (lstrides)[ULAB_MAX_DIMS - 3];\n",
+ " (rarray) -= (rstrides)[ULAB_MAX_DIMS - 2] * results->shape[ULAB_MAX_DIMS-2];\n",
+ " (rarray) += (rstrides)[ULAB_MAX_DIMS - 3];\n",
+ " j++;\n",
+ " } while(j < (results)->shape[ULAB_MAX_DIMS - 3]);\n",
+ "```\n",
+ "\n",
+ "In order to reduce firmware size, it *might* make sense in higher dimensions to make use of the function iterator by setting the \n",
+ "\n",
+ "```c\n",
+ "#define ULAB_HAS_FUNCTION_ITERATOR (1)\n",
+ "```\n",
+ "\n",
+ "constant to 1. This allows the compiler to call the `ndarray_rewind_array` function, so that it doesn't have to unwrap the loops for `k`, and `j`. Instead of the macro above, we now have \n",
+ "\n",
+ "```c\n",
+ "#define BINARY_LOOP(results, type_out, type_left, type_right, larray, lstrides, rarray, rstrides, OPERATOR)\n",
+ " type_out *array = (type_out *)(results)->array;\n",
+ " size_t *lcoords = ndarray_new_coords((results)->ndim);\n",
+ " size_t *rcoords = ndarray_new_coords((results)->ndim);\n",
+ " for(size_t i=0; i < (results)->len/(results)->shape[ULAB_MAX_DIMS -1]; i++) {\n",
+ " size_t l = 0;\n",
+ " do {\n",
+ " *array++ = *((type_left *)(larray)) OPERATOR *((type_right *)(rarray));\n",
+ " (larray) += (lstrides)[ULAB_MAX_DIMS - 1];\n",
+ " (rarray) += (rstrides)[ULAB_MAX_DIMS - 1];\n",
+ " l++;\n",
+ " } while(l < (results)->shape[ULAB_MAX_DIMS - 1]);\n",
+ " ndarray_rewind_array((results)->ndim, larray, (results)->shape, lstrides, lcoords);\n",
+ " ndarray_rewind_array((results)->ndim, rarray, (results)->shape, rstrides, rcoords);\n",
+ " } while(0)\n",
+ "```\n",
+ "\n",
+ "Since the `ndarray_rewind_array` function is implemented only once, a lot of space can be saved. Obviously, function calls cost time, thus such trade-offs must be evaluated for each application. The gain also depends on which functions and features you include. Operators and functions that involve two arrays are expensive, because at the C level, the number of cases that must be handled scales with the squares of the number of data types. As an example, the innocent-looking expression\n",
+ "\n",
+ "```python\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3])\n",
+ "b = np.array([4, 5, 6])\n",
+ "\n",
+ "c = a + b\n",
+ "```\n",
+ "requires 25 loops in C, because the `dtypes` of both `a`, and `b` can assume 5 different values, and the addition has to be resolved for all possible cases. Hint: each binary operator costs between 3 and 4 kB in two dimensions."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The ulab version string\n",
+ "\n",
+ "As is customary with `python` packages, information on the package version can be found be querying the `__version__` string. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T06:25:27.328061Z",
+ "start_time": "2021-01-12T06:25:27.308199Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "you are running ulab version 2.1.0-2D\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab\n",
+ "\n",
+ "print('you are running ulab version', ulab.__version__)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first three numbers indicate the major, minor, and sub-minor versions of `ulab` (defined by the `ULAB_VERSION` constant in [ulab.c](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.c)). We usually change the minor version, whenever a new function is added to the code, and the sub-minor version will be incremented, if a bug fix is implemented. \n",
+ "\n",
+ "`2D` tells us that the particular firmware supports tensors of rank 2 (defined by `ULAB_MAX_DIMS` in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h)). \n",
+ "\n",
+ "If you find a bug, please, include the version string in your report!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Should you need the numerical value of `ULAB_MAX_DIMS`, you can get it from the version string in the following way:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:00:00.616473Z",
+ "start_time": "2021-01-13T06:00:00.602787Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "version string: 2.1.0-2D\n",
+ "version dimensions: 2D\n",
+ "numerical value of dimensions: 2\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab\n",
+ "\n",
+ "version = ulab.__version__\n",
+ "version_dims = version.split('-')[1]\n",
+ "version_num = int(version_dims.replace('D', ''))\n",
+ "\n",
+ "print('version string: ', version)\n",
+ "print('version dimensions: ', version_dims)\n",
+ "print('numerical value of dimensions: ', version_num)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### ulab with complex arrays\n",
+ "\n",
+ "If the firmware supports complex arrays, `-c` is appended to the version string as can be seen below."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:21:04.079894Z",
+ "start_time": "2022-01-07T18:21:04.058855Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "version string: 4.0.0-2D-c\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab\n",
+ "\n",
+ "version = ulab.__version__\n",
+ "\n",
+ "print('version string: ', version)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Finding out what your firmware supports\n",
+ "\n",
+ "`ulab` implements a number of array operators and functions, but this does not mean that all of these functions and methods are actually compiled into the firmware. You can fine-tune your firmware by setting/unsetting any of the `_HAS_` constants in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h). \n",
+ "\n",
+ "### Functions included in the firmware\n",
+ "\n",
+ "The version string will not tell you everything about your firmware, because the supported functions and sub-modules can still arbitrarily be included or excluded. One way of finding out what is compiled into the firmware is calling `dir` with `ulab` as its argument."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:47:37.963507Z",
+ "start_time": "2021-01-08T12:47:37.936641Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===== constants, functions, and modules of numpy =====\n",
+ "\n",
+ " ['__class__', '__name__', 'bool', 'sort', 'sum', 'acos', 'acosh', 'arange', 'arctan2', 'argmax', 'argmin', 'argsort', 'around', 'array', 'asin', 'asinh', 'atan', 'atanh', 'ceil', 'clip', 'concatenate', 'convolve', 'cos', 'cosh', 'cross', 'degrees', 'diag', 'diff', 'e', 'equal', 'exp', 'expm1', 'eye', 'fft', 'flip', 'float', 'floor', 'frombuffer', 'full', 'get_printoptions', 'inf', 'int16', 'int8', 'interp', 'linalg', 'linspace', 'log', 'log10', 'log2', 'logspace', 'max', 'maximum', 'mean', 'median', 'min', 'minimum', 'nan', 'ndinfo', 'not_equal', 'ones', 'pi', 'polyfit', 'polyval', 'radians', 'roll', 'set_printoptions', 'sin', 'sinh', 'sqrt', 'std', 'tan', 'tanh', 'trapz', 'uint16', 'uint8', 'vectorize', 'zeros']\n",
+ "\n",
+ "functions included in the fft module:\n",
+ " ['__class__', '__name__', 'fft', 'ifft']\n",
+ "\n",
+ "functions included in the linalg module:\n",
+ " ['__class__', '__name__', 'cholesky', 'det', 'dot', 'eig', 'inv', 'norm', 'trace']\n",
+ "\n",
+ "\n",
+ "===== modules of scipy =====\n",
+ "\n",
+ " ['__class__', '__name__', 'optimize', 'signal', 'special']\n",
+ "\n",
+ "functions included in the optimize module:\n",
+ " ['__class__', '__name__', 'bisect', 'fmin', 'newton']\n",
+ "\n",
+ "functions included in the signal module:\n",
+ " ['__class__', '__name__', 'sosfilt', 'spectrogram']\n",
+ "\n",
+ "functions included in the special module:\n",
+ " ['__class__', '__name__', 'erf', 'erfc', 'gamma', 'gammaln']\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import scipy as spy\n",
+ "\n",
+ "\n",
+ "print('===== constants, functions, and modules of numpy =====\\n\\n', dir(np))\n",
+ "\n",
+ "# since fft and linalg are sub-modules, print them separately\n",
+ "print('\\nfunctions included in the fft module:\\n', dir(np.fft))\n",
+ "print('\\nfunctions included in the linalg module:\\n', dir(np.linalg))\n",
+ "\n",
+ "print('\\n\\n===== modules of scipy =====\\n\\n', dir(spy))\n",
+ "print('\\nfunctions included in the optimize module:\\n', dir(spy.optimize))\n",
+ "print('\\nfunctions included in the signal module:\\n', dir(spy.signal))\n",
+ "print('\\nfunctions included in the special module:\\n', dir(spy.special))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Methods included in the firmware\n",
+ "\n",
+ "The `dir` function applied to the module or its sub-modules gives information on what the module and sub-modules include, but is not enough to find out which methods the `ndarray` class supports. We can list the methods by calling `dir` with the `array` object itself:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:48:17.927709Z",
+ "start_time": "2021-01-08T12:48:17.903132Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "['__class__', '__name__', 'copy', 'sort', '__bases__', '__dict__', 'dtype', 'flatten', 'itemsize', 'reshape', 'shape', 'size', 'strides', 'tobytes', 'transpose']\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print(dir(np.array))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Operators included in the firmware\n",
+ "\n",
+ "A list of operators cannot be generated as shown above. If you really need to find out, whether, e.g., the `**` operator is supported by the firmware, you have to `try` it:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-08T12:49:59.902054Z",
+ "start_time": "2021-01-08T12:49:59.875760Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operator is not supported: unsupported types for __pow__: 'ndarray', 'ndarray'\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3])\n",
+ "b = np.array([4, 5, 6])\n",
+ "\n",
+ "try:\n",
+ " print(a ** b)\n",
+ "except Exception as e:\n",
+ " print('operator is not supported: ', e)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The exception above would be raised, if the firmware was compiled with the \n",
+ "\n",
+ "```c\n",
+ "#define NDARRAY_HAS_BINARY_OP_POWER (0)\n",
+ "```\n",
+ "\n",
+ "definition."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-ndarray.ipynb b/circuitpython/extmod/ulab/docs/ulab-ndarray.ipynb
new file mode 100644
index 0000000..7524e35
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-ndarray.ipynb
@@ -0,0 +1,3754 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:20:20.064769Z",
+ "start_time": "2021-01-12T16:20:19.787429Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:46:22.666663Z",
+ "start_time": "2022-01-07T18:46:22.663583Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T18:46:29.198681Z",
+ "start_time": "2022-01-07T18:46:29.177654Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# ndarray, the base class\n",
+ "\n",
+ "The `ndarray` is the underlying container of numerical data. It can be thought of as micropython's own `array` object, but has a great number of extra features starting with how it can be initialised, which operations can be done on it, and which functions can accept it as an argument. One important property of an `ndarray` is that it is also a proper `micropython` iterable.\n",
+ "\n",
+ "The `ndarray` consists of a short header, and a pointer that holds the data. The pointer always points to a contiguous segment in memory (`numpy` is more flexible in this regard), and the header tells the interpreter, how the data from this segment is to be read out, and what the bytes mean. Some operations, e.g., `reshape`, are fast, because they do not operate on the data, they work on the header, and therefore, only a couple of bytes are manipulated, even if there are a million data entries. A more detailed exposition of how operators are implemented can be found in the section titled [Programming ulab](#Programming_ula).\n",
+ "\n",
+ "Since the `ndarray` is a binary container, it is also compact, meaning that it takes only a couple of bytes of extra RAM in addition to what is required for storing the numbers themselves. `ndarray`s are also type-aware, i.e., one can save RAM by specifying a data type, and using the smallest reasonable one. Five such types are defined, namely `uint8`, `int8`, which occupy a single byte of memory per datum, `uint16`, and `int16`, which occupy two bytes per datum, and `float`, which occupies four or eight bytes per datum. The precision/size of the `float` type depends on the definition of `mp_float_t`. Some platforms, e.g., the PYBD, implement `double`s, but some, e.g., the pyboard.v.11, do not. You can find out, what type of float your particular platform implements by looking at the output of the [.itemsize](#.itemsize) class property, or looking at the exact `dtype`, when you print out an array.\n",
+ "\n",
+ "In addition to the five above-mentioned numerical types, it is also possible to define Boolean arrays, which can be used in the indexing of data. However, Boolean arrays are really nothing but arrays of type `uint8` with an extra flag. \n",
+ "\n",
+ "On the following pages, we will see how one can work with `ndarray`s. Those familiar with `numpy` should find that the nomenclature and naming conventions of `numpy` are adhered to as closely as possible. We will point out the few differences, where necessary.\n",
+ "\n",
+ "For the sake of comparison, in addition to the `ulab` code snippets, sometimes the equivalent `numpy` code is also presented. You can find out, where the snippet is supposed to run by looking at its first line, the header of the code block."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The ndinfo function\n",
+ "\n",
+ "A concise summary of a couple of the properties of an `ndarray` can be printed out by calling the `ndinfo` \n",
+ "function. In addition to finding out what the *shape* and *strides* of the array array, we also get the `itemsize`, as well as the type. An interesting piece of information is the *data pointer*, which tells us, what the address of the data segment of the `ndarray` is. We will see the significance of this in the section [Slicing and indexing](#Slicing-and-indexing).\n",
+ "\n",
+ "Note that this function simply prints some information, but does not return anything. If you need to get a handle of the data contained in the printout, you should call the dedicated `shape`, `strides`, or `itemsize` functions directly."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:24:08.710325Z",
+ "start_time": "2021-01-12T16:24:08.699287Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "class: ndarray\n",
+ "shape: (5,)\n",
+ "strides: (8,)\n",
+ "itemsize: 8\n",
+ "data pointer: 0x7f8f6fa2e240\n",
+ "type: float\n",
+ "\n",
+ "\n",
+ "class: ndarray\n",
+ "shape: (5, 5)\n",
+ "strides: (5, 1)\n",
+ "itemsize: 1\n",
+ "data pointer: 0x7f8f6fa2e2e0\n",
+ "type: uint8\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(5), dtype=np.float)\n",
+ "b = np.array(range(25), dtype=np.uint8).reshape((5, 5))\n",
+ "np.ndinfo(a)\n",
+ "print('\\n')\n",
+ "np.ndinfo(b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Initialising an array\n",
+ "\n",
+ "A new array can be created by passing either a standard micropython iterable, or another `ndarray` into the constructor."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Initialising by passing iterables\n",
+ "\n",
+ "If the iterable is one-dimensional, i.e., one whose elements are numbers, then a row vector will be created and returned. If the iterable is two-dimensional, i.e., one whose elements are again iterables, a matrix will be created. If the lengths of the iterables are not consistent, a `ValueError` will be raised. Iterables of different types can be mixed in the initialisation function. \n",
+ "\n",
+ "If the `dtype` keyword with the possible `uint8/int8/uint16/int16/float` values is supplied, the new `ndarray` will have that type, otherwise, it assumes `float` as default. In addition, if `ULAB_SUPPORTS_COMPLEX` is set to 1 in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h), the `dtype` can also take on the value of `complex`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:24:21.952689Z",
+ "start_time": "2021-01-12T16:24:21.938231Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t [1, 2, 3, 4, 5, 6, 7, 8]\n",
+ "b:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "\n",
+ "c:\t array([[0, 1, 2, 3, 4],\n",
+ " [20, 21, 22, 23, 24],\n",
+ " [44, 55, 66, 77, 88]], dtype=uint8)\n",
+ "\n",
+ "Traceback (most recent call last):\n",
+ " File \"/dev/shm/micropython.py\", line 15, in <module>\n",
+ "ValueError: iterables are not of the same length\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = [1, 2, 3, 4, 5, 6, 7, 8]\n",
+ "b = np.array(a)\n",
+ "\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"b:\\t\", b)\n",
+ "\n",
+ "# a two-dimensional array with mixed-type initialisers\n",
+ "c = np.array([range(5), range(20, 25, 1), [44, 55, 66, 77, 88]], dtype=np.uint8)\n",
+ "print(\"\\nc:\\t\", c)\n",
+ "\n",
+ "# and now we throw an exception\n",
+ "d = np.array([range(5), range(10), [44, 55, 66, 77, 88]], dtype=np.uint8)\n",
+ "print(\"\\nd:\\t\", d)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Initialising by passing arrays\n",
+ "\n",
+ "An `ndarray` can be initialised by supplying another array. This statement is almost trivial, since `ndarray`s are iterables themselves, though it should be pointed out that initialising through arrays is a bit faster. This statement is especially true, if the `dtype`s of the source and output arrays are the same, because then the contents can simply be copied without further ado. While type conversion is also possible, it will always be slower than straight copying."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:24:33.050654Z",
+ "start_time": "2021-01-12T16:24:33.039754Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t [1, 2, 3, 4, 5, 6, 7, 8]\n",
+ "\n",
+ "b:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "\n",
+ "c:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
+ "\n",
+ "d:\t array([1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = [1, 2, 3, 4, 5, 6, 7, 8]\n",
+ "b = np.array(a)\n",
+ "c = np.array(b)\n",
+ "d = np.array(b, dtype=np.uint8)\n",
+ "\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"\\nb:\\t\", b)\n",
+ "print(\"\\nc:\\t\", c)\n",
+ "print(\"\\nd:\\t\", d)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that the default type of the `ndarray` is `float`. Hence, if the array is initialised from another array, type conversion will always take place, except, when the output type is specifically supplied. I.e., "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:24:39.722844Z",
+ "start_time": "2021-01-12T16:24:39.709963Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([0, 1, 2, 3, 4], dtype=uint8)\n",
+ "\n",
+ "b:\t array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(5), dtype=np.uint8)\n",
+ "b = np.array(a)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"\\nb:\\t\", b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "will iterate over the elements in `a`, since in the assignment `b = np.array(a)`, no output type was given, therefore, `float` was assumed. On the other hand, "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:25:06.597051Z",
+ "start_time": "2021-01-12T16:25:06.585511Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([0, 1, 2, 3, 4], dtype=uint8)\n",
+ "\n",
+ "b:\t array([0, 1, 2, 3, 4], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(5), dtype=np.uint8)\n",
+ "b = np.array(a, dtype=np.uint8)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"\\nb:\\t\", b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "will simply copy the content of `a` into `b` without any iteration, and will, therefore, be faster. Keep this in mind, whenever the output type, or performance is important."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Array initialisation functions\n",
+ "\n",
+ "There are nine functions that can be used for initialising an array. Starred functions accept `complex` as the value of the `dtype`, if the firmware was compiled with complex support.\n",
+ "\n",
+ "1. [numpy.arange](#arange)\n",
+ "1. [numpy.concatenate](#concatenate)\n",
+ "1. [numpy.diag*](#diag)\n",
+ "1. [numpy.empty*](#empty)\n",
+ "1. [numpy.eye*](#eye)\n",
+ "1. [numpy.frombuffer](#frombuffer)\n",
+ "1. [numpy.full*](#full)\n",
+ "1. [numpy.linspace*](#linspace)\n",
+ "1. [numpy.logspace](#logspace)\n",
+ "1. [numpy.ones*](#ones)\n",
+ "1. [numpy.zeros*](#zeros)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### arange\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.arange.html\n",
+ "\n",
+ "The function returns a one-dimensional array with evenly spaced values. Takes 3 positional arguments (two are optional), and the `dtype` keyword argument. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:26:03.795728Z",
+ "start_time": "2021-01-12T16:26:03.782352Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)\n",
+ "array([2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)\n",
+ "array([2, 5, 8], dtype=int16)\n",
+ "array([2.0, 5.0, 8.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print(np.arange(10))\n",
+ "print(np.arange(2, 10))\n",
+ "print(np.arange(2, 10, 3))\n",
+ "print(np.arange(2, 10, 3, dtype=np.float))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### concatenate\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.concatenate.html\n",
+ "\n",
+ "The function joins a sequence of arrays, if they are compatible in shape, i.e., if all shapes except the one along the joining axis are equal. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:26:37.145965Z",
+ "start_time": "2021-01-12T16:26:37.134350Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[0, 1, 2, 3, 4],\n",
+ " [5, 6, 7, 8, 9],\n",
+ " [10, 11, 12, 13, 14],\n",
+ " [15, 16, 17, 18, 19],\n",
+ " [20, 21, 22, 23, 24],\n",
+ " [0, 1, 2, 3, 4],\n",
+ " [5, 6, 7, 8, 9],\n",
+ " [10, 11, 12, 13, 14]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(25), dtype=np.uint8).reshape((5, 5))\n",
+ "b = np.array(range(15), dtype=np.uint8).reshape((3, 5))\n",
+ "\n",
+ "c = np.concatenate((a, b), axis=0)\n",
+ "print(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**WARNING**: `numpy` accepts arbitrary `dtype`s in the sequence of arrays, in `ulab` the `dtype`s must be identical. If you want to concatenate different types, you have to convert all arrays to the same type first. Here `b` is of `float` type, so it cannot directly be concatenated to `a`. However, if we cast the `dtype` of `b`, the concatenation works:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:26:56.120820Z",
+ "start_time": "2021-01-12T16:26:56.102365Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([[0, 1, 2, 3, 4],\n",
+ " [5, 6, 7, 8, 9],\n",
+ " [10, 11, 12, 13, 14],\n",
+ " [15, 16, 17, 18, 19],\n",
+ " [20, 21, 22, 23, 24]], dtype=uint8)\n",
+ "====================\n",
+ "d: array([[1, 2, 3],\n",
+ " [4, 5, 6],\n",
+ " [7, 8, 9],\n",
+ " [10, 11, 12],\n",
+ " [13, 14, 15]], dtype=uint8)\n",
+ "====================\n",
+ "c: array([[1, 2, 3, 0, 1, 2, 3, 4],\n",
+ " [4, 5, 6, 5, 6, 7, 8, 9],\n",
+ " [7, 8, 9, 10, 11, 12, 13, 14],\n",
+ " [10, 11, 12, 15, 16, 17, 18, 19],\n",
+ " [13, 14, 15, 20, 21, 22, 23, 24]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(25), dtype=np.uint8).reshape((5, 5))\n",
+ "b = np.array(range(15), dtype=np.float).reshape((5, 3))\n",
+ "d = np.array(b+1, dtype=np.uint8)\n",
+ "print('a: ', a)\n",
+ "print('='*20 + '\\nd: ', d)\n",
+ "c = np.concatenate((d, a), axis=1)\n",
+ "print('='*20 + '\\nc: ', c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## diag\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.diag.html\n",
+ "\n",
+ "Extract a diagonal, or construct a diagonal array.\n",
+ "\n",
+ "The function takes two arguments, an `ndarray`, and a shift. If the first argument is a two-dimensional array, the function returns a one-dimensional array containing the diagonal entries. The diagonal can be shifted by an amount given in the second argument.\n",
+ "\n",
+ "If the first argument is a one-dimensional array, the function returns a two-dimensional tensor with its diagonal elements given by the first argument.\n",
+ "\n",
+ "The `diag` function can accept a complex array, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[1.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 2.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 3.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 4.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4])\n",
+ "print(np.diag(a))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([[0.0, 1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0, 7.0],\n",
+ " [8.0, 9.0, 10.0, 11.0],\n",
+ " [12.0, 13.0, 14.0, 15.0]], dtype=float64)\n",
+ "\n",
+ "diagonal of a: array([0.0, 5.0, 10.0, 15.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(16)).reshape((4, 4))\n",
+ "print('a: ', a)\n",
+ "print()\n",
+ "print('diagonal of a: ', np.diag(a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## empty\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.empty.html\n",
+ "\n",
+ "`empty` is simply an alias for `zeros`, i.e., as opposed to `numpy`, the entries of the tensor will be initialised to zero. \n",
+ "\n",
+ "The `empty` function can accept complex as the value of the dtype, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### eye\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.eye.html\n",
+ "\n",
+ "Another special array method is the `eye` function, whose call signature is \n",
+ "\n",
+ "```python\n",
+ "eye(N, M, k=0, dtype=float)\n",
+ "```\n",
+ "where `N` (`M`) specify the dimensions of the matrix (if only `N` is supplied, then we get a square matrix, otherwise one with `M` rows, and `N` columns), and `k` is the shift of the ones (the main diagonal corresponds to `k=0`). Here are a couple of examples.\n",
+ "\n",
+ "The `eye` function can accept `complex` as the value of the `dtype`, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### With a single argument"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:27:08.533394Z",
+ "start_time": "2021-01-12T16:27:08.518940Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[1.0, 0.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 1.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 1.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 1.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 0.0, 1.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print(np.eye(5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Specifying the dimensions of the matrix"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:27:34.075468Z",
+ "start_time": "2021-01-12T16:27:34.064137Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[0, 0, 0, 0, 0, 0],\n",
+ " [1, 0, 0, 0, 0, 0],\n",
+ " [0, 1, 0, 0, 0, 0],\n",
+ " [0, 0, 1, 0, 0, 0]], dtype=int16)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print(np.eye(4, M=6, k=-1, dtype=np.int16))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:27:42.492135Z",
+ "start_time": "2021-01-12T16:27:42.477684Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[1, 0, 0, 0, 0, 0],\n",
+ " [0, 1, 0, 0, 0, 0],\n",
+ " [0, 0, 1, 0, 0, 0],\n",
+ " [0, 0, 0, 1, 0, 0]], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print(np.eye(4, M=6, dtype=np.int8))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### frombuffer\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.frombuffer.html\n",
+ "\n",
+ "The function interprets a contiguous buffer as a one-dimensional array, and thus can be used for piping buffered data directly into an array. This method of analysing, e.g., ADC data is much more efficient than passing the ADC buffer into the `array` constructor, because `frombuffer` simply creates the `ndarray` header and blindly copies the memory segment, without inspecting the underlying data. \n",
+ "\n",
+ "The function takes a single positional argument, the buffer, and three keyword arguments. These are the `dtype` with a default value of `float`, the `offset`, with a default of 0, and the `count`, with a default of -1, meaning that all data are taken in."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-15T07:01:35.320458Z",
+ "start_time": "2021-01-15T07:01:35.307407Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "buffer: b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
+ "a, all data read: array([1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "b, all data with an offset: array([3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "c, only 3 items with an offset: array([3, 4, 5], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "buffer = b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
+ "print('buffer: ', buffer)\n",
+ "\n",
+ "a = np.frombuffer(buffer, dtype=np.uint8)\n",
+ "print('a, all data read: ', a)\n",
+ "\n",
+ "b = np.frombuffer(buffer, dtype=np.uint8, offset=2)\n",
+ "print('b, all data with an offset: ', b)\n",
+ "\n",
+ "c = np.frombuffer(buffer, dtype=np.uint8, offset=2, count=3)\n",
+ "print('c, only 3 items with an offset: ', c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### full\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.full.html\n",
+ "\n",
+ "The function returns an array of arbitrary dimension, whose elements are all equal to the second positional argument. The first argument is a tuple describing the shape of the tensor. The `dtype` keyword argument with a default value of `float` can also be supplied.\n",
+ "\n",
+ "The `full` function can accept a complex scalar, or `complex` as the value of `dtype`, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:29:11.931011Z",
+ "start_time": "2021-01-12T16:29:11.915195Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([[3.0, 3.0, 3.0, 3.0],\n",
+ " [3.0, 3.0, 3.0, 3.0]], dtype=float64)\n",
+ "\n",
+ "====================\n",
+ "\n",
+ "array([[3, 3, 3, 3],\n",
+ " [3, 3, 3, 3]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "# create an array with the default type\n",
+ "print(np.full((2, 4), 3))\n",
+ "\n",
+ "print('\\n' + '='*20 + '\\n')\n",
+ "# the array type is uint8 now\n",
+ "print(np.full((2, 4), 3, dtype=np.uint8))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### linspace\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html\n",
+ "\n",
+ "This function returns an array, whose elements are uniformly spaced between the `start`, and `stop` points. The number of intervals is determined by the `num` keyword argument, whose default value is 50. With the `endpoint` keyword argument (defaults to `True`) one can include `stop` in the sequence. In addition, the `dtype` keyword can be supplied to force type conversion of the output. The default is `float`. Note that, when `dtype` is of integer type, the sequence is not necessarily evenly spaced. This is not an error, rather a consequence of rounding. (This is also the `numpy` behaviour.)\n",
+ "\n",
+ "The `linspace` function can accept `complex` as the value of the `dtype`, if the firmware was compiled with complex support. The output `dtype` is automatically complex, if either of the endpoints is a complex scalar."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:29:45.897927Z",
+ "start_time": "2021-01-12T16:29:45.876325Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "default sequence:\t array([0.0, 0.2040816326530612, 0.4081632653061225, ..., 9.591836734693871, 9.795918367346932, 9.999999999999993], dtype=float64)\n",
+ "num=5:\t\t\t array([0.0, 2.5, 5.0, 7.5, 10.0], dtype=float64)\n",
+ "num=5:\t\t\t array([0.0, 2.0, 4.0, 6.0, 8.0], dtype=float64)\n",
+ "num=5:\t\t\t array([0, 0, 1, 2, 2, 3, 4], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "# generate a sequence with defaults\n",
+ "print('default sequence:\\t', np.linspace(0, 10))\n",
+ "\n",
+ "# num=5\n",
+ "print('num=5:\\t\\t\\t', np.linspace(0, 10, num=5))\n",
+ "\n",
+ "# num=5, endpoint=False\n",
+ "print('num=5:\\t\\t\\t', np.linspace(0, 10, num=5, endpoint=False))\n",
+ "\n",
+ "# num=5, endpoint=False, dtype=uint8\n",
+ "print('num=5:\\t\\t\\t', np.linspace(0, 5, num=7, endpoint=False, dtype=np.uint8))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### logspace\n",
+ "\n",
+ "`linspace`' equivalent for logarithmically spaced data is `logspace`. This function produces a sequence of numbers, in which the quotient of consecutive numbers is constant. This is a geometric sequence.\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.logspace.html\n",
+ "\n",
+ "This function returns an array, whose elements are uniformly spaced between the `start`, and `stop` points. The number of intervals is determined by the `num` keyword argument, whose default value is 50. With the `endpoint` keyword argument (defaults to `True`) one can include `stop` in the sequence. In addition, the `dtype` keyword can be supplied to force type conversion of the output. The default is `float`. Note that, exactly as in `linspace`, when `dtype` is of integer type, the sequence is not necessarily evenly spaced in log space.\n",
+ "\n",
+ "In addition to the keyword arguments found in `linspace`, `logspace` also accepts the `base` argument. The default value is 10. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:30:44.483893Z",
+ "start_time": "2021-01-12T16:30:44.466705Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "default sequence:\t array([1.0, 1.151395399326447, 1.325711365590109, ..., 754.3120063354646, 868.5113737513561, 1000.000000000004], dtype=float64)\n",
+ "num=5:\t\t\t array([10.0, 1778.279410038923, 316227.766016838, 56234132.5190349, 10000000000.0], dtype=float64)\n",
+ "num=5:\t\t\t array([10.0, 630.9573444801933, 39810.71705534974, 2511886.431509581, 158489319.2461114], dtype=float64)\n",
+ "num=5:\t\t\t array([2.0, 6.964404506368993, 24.25146506416637, 84.44850628946524, 294.066778879241], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "# generate a sequence with defaults\n",
+ "print('default sequence:\\t', np.logspace(0, 3))\n",
+ "\n",
+ "# num=5\n",
+ "print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5))\n",
+ "\n",
+ "# num=5, endpoint=False\n",
+ "print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5, endpoint=False))\n",
+ "\n",
+ "# num=5, endpoint=False\n",
+ "print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5, endpoint=False, base=2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### ones, zeros\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.zeros.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ones.html\n",
+ "\n",
+ "A couple of special arrays and matrices can easily be initialised by calling one of the `ones`, or `zeros` functions. `ones` and `zeros` follow the same pattern, and have the call signature\n",
+ "\n",
+ "```python\n",
+ "ones(shape, dtype=float)\n",
+ "zeros(shape, dtype=float)\n",
+ "```\n",
+ "where shape is either an integer, or a tuple specifying the shape.\n",
+ "\n",
+ "The `ones/zeros` functions can accept complex as the value of the dtype, if the firmware was compiled with complex support."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-12T16:32:05.422109Z",
+ "start_time": "2021-01-12T16:32:05.407921Z"
+ },
+ "scrolled": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([1, 1, 1, 1, 1, 1], dtype=uint8)\n",
+ "array([[0.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 0.0],\n",
+ " [0.0, 0.0, 0.0, 0.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "print(np.ones(6, dtype=np.uint8))\n",
+ "\n",
+ "print(np.zeros((6, 4)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ " When specifying the shape, make sure that the length of the tuple is not larger than the maximum dimension of your firmware."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:01:44.960353Z",
+ "start_time": "2021-01-13T06:01:44.944935Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "maximum number of dimensions: 2.1.0-2D\n",
+ "\n",
+ "Traceback (most recent call last):\n",
+ " File \"/dev/shm/micropython.py\", line 7, in <module>\n",
+ "TypeError: too many dimensions\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "import ulab\n",
+ "\n",
+ "print('maximum number of dimensions: ', ulab.__version__)\n",
+ "\n",
+ "print(np.zeros((2, 2, 2)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Customising array printouts"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`ndarray`s are pretty-printed, i.e., if the number of entries along the last axis is larger than 10 (default value), then only the first and last three entries will be printed. Also note that, as opposed to `numpy`, the printout always contains the `dtype`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:02:20.162127Z",
+ "start_time": "2021-01-13T06:02:20.146219Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([0.0, 1.0, 2.0, ..., 197.0, 198.0, 199.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(200))\n",
+ "print(\"a:\\t\", a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### set_printoptions\n",
+ "\n",
+ "The default values can be overwritten by means of the `set_printoptions` function [numpy.set_printoptions](https://numpy.org/doc/1.18/reference/generated/numpy.set_printoptions.html), which accepts two keywords arguments, the `threshold`, and the `edgeitems`. The first of these arguments determines the length of the longest array that will be printed in full, while the second is the number of items that will be printed on the left and right hand side of the ellipsis, if the array is longer than `threshold`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:02:42.073823Z",
+ "start_time": "2021-01-13T06:02:42.057424Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a printed with defaults:\t array([0.0, 1.0, 2.0, ..., 17.0, 18.0, 19.0], dtype=float64)\n",
+ "\n",
+ "a printed in full:\t\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0], dtype=float64)\n",
+ "\n",
+ "a truncated with 2 edgeitems:\t array([0.0, 1.0, ..., 18.0, 19.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(20))\n",
+ "print(\"a printed with defaults:\\t\", a)\n",
+ "\n",
+ "np.set_printoptions(threshold=200)\n",
+ "print(\"\\na printed in full:\\t\\t\", a)\n",
+ "\n",
+ "np.set_printoptions(threshold=10, edgeitems=2)\n",
+ "print(\"\\na truncated with 2 edgeitems:\\t\", a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### get_printoptions\n",
+ "\n",
+ "The set value of the `threshold` and `edgeitems` can be retrieved by calling the `get_printoptions` function with no arguments. The function returns a *dictionary* with two keys."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:02:51.383653Z",
+ "start_time": "2021-01-13T06:02:51.372551Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'threshold': 100, 'edgeitems': 20}\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "np.set_printoptions(threshold=100, edgeitems=20)\n",
+ "print(np.get_printoptions())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Methods and properties of ndarrays\n",
+ "\n",
+ "Arrays have several *properties* that can queried, and some methods that can be called. With the exception of the flatten and transpose operators, properties return an object that describe some feature of the array, while the methods return a new array-like object. The `imag`, and `real` properties are included in the firmware only, when it was compiled with complex support.\n",
+ "\n",
+ "1. [.byteswap](#.byteswap)\n",
+ "1. [.copy](#.copy)\n",
+ "1. [.dtype](#.dtype)\n",
+ "1. [.flat](#.flat)\n",
+ "1. [.flatten](#.flatten)\n",
+ "1. [.imag*](#.imag)\n",
+ "1. [.itemsize](#.itemsize)\n",
+ "1. [.real*](#.real)\n",
+ "1. [.reshape](#.reshape)\n",
+ "1. [.shape](#.shape)\n",
+ "1. [.size](#.size)\n",
+ "1. [.T](#.transpose)\n",
+ "1. [.tobytes](#.tobytes)\n",
+ "1. [.tolist](#.tolist)\n",
+ "1. [.transpose](#.transpose)\n",
+ "1. [.sort](#.sort)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .byteswap\n",
+ "\n",
+ "`numpy` https://numpy.org/doc/stable/reference/generated/numpy.char.chararray.byteswap.html\n",
+ "\n",
+ "The method takes a single keyword argument, `inplace`, with values `True` or `False`, and swaps the bytes in the array. If `inplace = False`, a new `ndarray` is returned, otherwise the original values are overwritten.\n",
+ "\n",
+ "The `frombuffer` function is a convenient way of receiving data from peripheral devices that work with buffers. However, it is not guaranteed that the byte order (in other words, the _endianness_) of the peripheral device matches that of the microcontroller. The `.byteswap` method makes it possible to change the endianness of the incoming data stream.\n",
+ "\n",
+ "Obviously, byteswapping makes sense only for those cases, when a datum occupies more than one byte, i.e., for the `uint16`, `int16`, and `float` `dtype`s. When `dtype` is either `uint8`, or `int8`, the method simply returns a view or copy of self, depending upon the value of `inplace`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-02-15T16:06:20.409727Z",
+ "start_time": "2021-02-15T16:06:20.398057Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "buffer: b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
+ "a: array([513, 1027, 1541, 2055], dtype=uint16)\n",
+ "b: array([258, 772, 1286, 1800], dtype=uint16)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "buffer = b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
+ "print('buffer: ', buffer)\n",
+ "\n",
+ "a = np.frombuffer(buffer, dtype=np.uint16)\n",
+ "print('a: ', a)\n",
+ "b = a.byteswap()\n",
+ "print('b: ', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .copy\n",
+ "\n",
+ "The `.copy` method creates a new *deep copy* of an array, i.e., the entries of the source array are *copied* into the target array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 34,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:02:58.898485Z",
+ "start_time": "2021-01-13T06:02:58.878864Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([1, 2, 3, 4], dtype=int8)\n",
+ "====================\n",
+ "b: array([1, 2, 3, 4], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "b = a.copy()\n",
+ "print('a: ', a)\n",
+ "print('='*20)\n",
+ "print('b: ', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .dtype\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.dtype.htm\n",
+ "\n",
+ "The `.dtype` property is the `dtype` of an array. This can then be used for initialising another array with the matching type. `ulab` implements two versions of `dtype`; one that is `numpy`-like, i.e., one, which returns a `dtype` object, and one that is significantly cheaper in terms of flash space, but does not define a `dtype` object, and holds a single character (number) instead. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-11-02T17:16:12.818777Z",
+ "start_time": "2020-11-02T17:16:12.807147Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([1, 2, 3, 4], dtype=int8)\n",
+ "dtype of a: dtype('int8')\n",
+ "\n",
+ "b: array([5, 6, 7], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "b = np.array([5, 6, 7], dtype=a.dtype)\n",
+ "print('a: ', a)\n",
+ "print('dtype of a: ', a.dtype)\n",
+ "print('\\nb: ', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If the `ulab.h` header file sets the pre-processor constant `ULAB_HAS_DTYPE_OBJECT` to 0 as\n",
+ "\n",
+ "```c\n",
+ "#define ULAB_HAS_DTYPE_OBJECT (0)\n",
+ "```\n",
+ "then the output of the previous snippet will be"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-11-02T20:36:23.099166Z",
+ "start_time": "2020-11-02T20:36:23.088586Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([1, 2, 3, 4], dtype=int8)\n",
+ "dtype of a: 98\n",
+ "\n",
+ "b: array([5, 6, 7], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "b = np.array([5, 6, 7], dtype=a.dtype)\n",
+ "print('a: ', a)\n",
+ "print('dtype of a: ', a.dtype)\n",
+ "print('\\nb: ', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here 98 is nothing but the ASCII value of the character `b`, which is the type code for signed 8-bit integers. The object definition adds around 600 bytes to the firmware."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .flat\n",
+ "\n",
+ "numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flat.htm\n",
+ "\n",
+ "`.flat` returns the array's flat iterator. For one-dimensional objects the flat iterator is equivalent to the standart iterator, while for higher dimensional tensors, it amounts to first flattening the array, and then iterating over it. Note, however, that the flat iterator does not consume RAM beyond what is required for holding the position of the iterator itself, while flattening produces a new copy."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1\n",
+ "2\n",
+ "3\n",
+ "4\n",
+ "a:\n",
+ " array([[1, 2, 3, 4],\n",
+ " [5, 6, 7, 8]], dtype=int8)\n",
+ "array([1, 2, 3, 4], dtype=int8)\n",
+ "array([5, 6, 7, 8], dtype=int8)\n",
+ "1\n",
+ "2\n",
+ "3\n",
+ "4\n",
+ "5\n",
+ "6\n",
+ "7\n",
+ "8\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "for _a in a:\n",
+ " print(_a)\n",
+ "\n",
+ "a = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.int8)\n",
+ "print('a:\\n', a)\n",
+ "\n",
+ "for _a in a:\n",
+ " print(_a)\n",
+ "\n",
+ "for _a in a.flat:\n",
+ " print(_a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .flatten\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flatten.htm\n",
+ "\n",
+ "`.flatten` returns the flattened array. The array can be flattened in `C` style (i.e., moving along the last axis in the tensor), or in `fortran` style (i.e., moving along the first axis in the tensor)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:07:16.735771Z",
+ "start_time": "2021-01-13T06:07:16.723514Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \t\t array([1, 2, 3, 4], dtype=int8)\n",
+ "a flattened: \t array([1, 2, 3, 4], dtype=int8)\n",
+ "\n",
+ "b: array([[1, 2, 3],\n",
+ " [4, 5, 6]], dtype=int8)\n",
+ "b flattened (C): \t array([1, 2, 3, 4, 5, 6], dtype=int8)\n",
+ "b flattened (F): \t array([1, 4, 2, 5, 3, 6], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "print(\"a: \\t\\t\", a)\n",
+ "print(\"a flattened: \\t\", a.flatten())\n",
+ "\n",
+ "b = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int8)\n",
+ "print(\"\\nb:\", b)\n",
+ "\n",
+ "print(\"b flattened (C): \\t\", b.flatten())\n",
+ "print(\"b flattened (F): \\t\", b.flatten(order='F'))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .imag\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.imag.html\n",
+ "\n",
+ "The `.imag` property is defined only, if the firmware was compiled with complex support, and returns a copy with the imaginary part of an array. If the array is real, then the output is straight zeros with the `dtype` of the input. If the input is complex, the output `dtype` is always `float`, irrespective of the values."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:07:26.171208Z",
+ "start_time": "2022-01-07T19:07:26.152633Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([1, 2, 3], dtype=uint16)\n",
+ "a.imag:\t array([0, 0, 0], dtype=uint16)\n",
+ "\n",
+ "b:\t array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)\n",
+ "b.imag:\t array([0.0, 1.0, -1.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3], dtype=np.uint16)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"a.imag:\\t\", a.imag)\n",
+ "\n",
+ "b = np.array([1, 2+1j, 3-1j], dtype=np.complex)\n",
+ "print(\"\\nb:\\t\", b)\n",
+ "print(\"b.imag:\\t\", b.imag)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .itemsize\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.itemsize.html\n",
+ "\n",
+ "The `.itemsize` property is an integer with the size of elements in the array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:07:49.080817Z",
+ "start_time": "2021-01-13T06:07:49.065749Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([1, 2, 3], dtype=int8)\n",
+ "itemsize of a: 1\n",
+ "\n",
+ "b:\n",
+ " array([[1.0, 2.0],\n",
+ " [3.0, 4.0]], dtype=float64)\n",
+ "itemsize of b: 8\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3], dtype=np.int8)\n",
+ "print(\"a:\\n\", a)\n",
+ "print(\"itemsize of a:\", a.itemsize)\n",
+ "\n",
+ "b= np.array([[1, 2], [3, 4]], dtype=np.float)\n",
+ "print(\"\\nb:\\n\", b)\n",
+ "print(\"itemsize of b:\", b.itemsize)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .real\n",
+ "\n",
+ "numpy: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.real.html\n",
+ "\n",
+ "The `.real` property is defined only, if the firmware was compiled with complex support, and returns a copy with the real part of an array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:10:01.870921Z",
+ "start_time": "2022-01-07T19:10:01.860071Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([1, 2, 3], dtype=uint16)\n",
+ "a.real:\t array([1, 2, 3], dtype=uint16)\n",
+ "\n",
+ "b:\t array([1.0+0.0j, 2.0+1.0j, 3.0-1.0j], dtype=complex)\n",
+ "b.real:\t array([1.0, 2.0, 3.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3], dtype=np.uint16)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"a.real:\\t\", a.real)\n",
+ "\n",
+ "b = np.array([1, 2+1j, 3-1j], dtype=np.complex)\n",
+ "print(\"\\nb:\\t\", b)\n",
+ "print(\"b.real:\\t\", b.real)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .reshape\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html\n",
+ "\n",
+ "`reshape` re-writes the shape properties of an `ndarray`, but the array will not be modified in any other way. The function takes a single 2-tuple with two integers as its argument. The 2-tuple should specify the desired number of rows and columns. If the new shape is not consistent with the old, a `ValueError` exception will be raised."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:08:12.234490Z",
+ "start_time": "2021-01-13T06:08:12.217652Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a (4 by 4): array([[1, 2, 3, 4],\n",
+ " [5, 6, 7, 8],\n",
+ " [9, 10, 11, 12],\n",
+ " [13, 14, 15, 16]], dtype=uint8)\n",
+ "a (2 by 8): array([[1, 2, 3, 4, 5, 6, 7, 8],\n",
+ " [9, 10, 11, 12, 13, 14, 15, 16]], dtype=uint8)\n",
+ "a (1 by 16): array([[1, 2, 3, ..., 14, 15, 16]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]], dtype=np.uint8)\n",
+ "print('a (4 by 4):', a)\n",
+ "print('a (2 by 8):', a.reshape((2, 8)))\n",
+ "print('a (1 by 16):', a.reshape((1, 16)))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Note that `ndarray.reshape()` can also be called by assigning to `ndarray.shape`. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .shape\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.shape.html\n",
+ "\n",
+ "The `.shape` property is a tuple whose elements are the length of the array along each axis. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:08:50.479850Z",
+ "start_time": "2021-01-13T06:08:50.464741Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([1, 2, 3, 4], dtype=int8)\n",
+ "shape of a: (4,)\n",
+ "\n",
+ "b:\n",
+ " array([[1, 2],\n",
+ " [3, 4]], dtype=int8)\n",
+ "shape of b: (2, 2)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "print(\"a:\\n\", a)\n",
+ "print(\"shape of a:\", a.shape)\n",
+ "\n",
+ "b= np.array([[1, 2], [3, 4]], dtype=np.int8)\n",
+ "print(\"\\nb:\\n\", b)\n",
+ "print(\"shape of b:\", b.shape)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "By assigning a tuple to the `.shape` property, the array can be `reshape`d:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], dtype=float64)\n",
+ "\n",
+ "a:\n",
+ " array([[1.0, 2.0, 3.0],\n",
+ " [4.0, 5.0, 6.0],\n",
+ " [7.0, 8.0, 9.0]], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])\n",
+ "print('a:\\n', a)\n",
+ "\n",
+ "a.shape = (3, 3)\n",
+ "print('\\na:\\n', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .size\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.size.html\n",
+ "\n",
+ "The `.size` property is an integer specifying the number of elements in the array. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-02-11T06:32:22.721112Z",
+ "start_time": "2020-02-11T06:32:22.713111Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([1, 2, 3], dtype=int8)\n",
+ "size of a: 3\n",
+ "\n",
+ "b:\n",
+ " array([[1, 2],\n",
+ "\t [3, 4]], dtype=int8)\n",
+ "size of b: 4\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3], dtype=np.int8)\n",
+ "print(\"a:\\n\", a)\n",
+ "print(\"size of a:\", a.size)\n",
+ "\n",
+ "b= np.array([[1, 2], [3, 4]], dtype=np.int8)\n",
+ "print(\"\\nb:\\n\", b)\n",
+ "print(\"size of b:\", b.size)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ ".T\n",
+ "\n",
+ "The `.T` property of the `ndarray` is equivalent to [.transpose](#.transpose)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .tobytes\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tobytes.html\n",
+ "\n",
+ "The `.tobytes` method can be used for acquiring a handle of the underlying data pointer of an array, and it returns a new `bytearray` that can be fed into any method that can accep a `bytearray`, e.g., ADC data can be buffered into this `bytearray`, or the `bytearray` can be fed into a DAC. Since the `bytearray` is really nothing but the bare data container of the array, any manipulation on the `bytearray` automatically modifies the array itself.\n",
+ "\n",
+ "Note that the method raises a `ValueError` exception, if the array is not dense (i.e., it has already been sliced)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 46,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:09:57.262071Z",
+ "start_time": "2021-01-13T06:09:57.250519Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([0, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)\n",
+ "b: bytearray(b'\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07')\n",
+ "====================\n",
+ "b: bytearray(b'\\r\\x01\\x02\\x03\\x04\\x05\\x06\\x07')\n",
+ "a: array([13, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(8), dtype=np.uint8)\n",
+ "print('a: ', a)\n",
+ "b = a.tobytes()\n",
+ "print('b: ', b)\n",
+ "\n",
+ "# modify b\n",
+ "b[0] = 13\n",
+ "\n",
+ "print('='*20)\n",
+ "print('b: ', b)\n",
+ "print('a: ', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .tolist\n",
+ "\n",
+ "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tolist.html\n",
+ "\n",
+ "The `.tolist` method can be used for converting the numerical array into a (nested) `python` lists."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:01:28.671234Z",
+ "start_time": "2022-01-07T19:01:28.568786Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([0, 1, 2, 3], dtype=uint8)\n",
+ "b: [0, 1, 2, 3]\n",
+ "====================\n",
+ "c: array([[0, 1],\n",
+ " [2, 3]], dtype=uint8)\n",
+ "d: [[0, 1], [2, 3]]\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(4), dtype=np.uint8)\n",
+ "print('a: ', a)\n",
+ "b = a.tolist()\n",
+ "print('b: ', b)\n",
+ "\n",
+ "c = a.reshape((2, 2))\n",
+ "print('='*20)\n",
+ "print('c: ', c)\n",
+ "d = c.tolist()\n",
+ "print('d: ', d)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .transpose\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.transpose.html\n",
+ "\n",
+ "Returns the transposed array. Only defined, if the number of maximum dimensions is larger than 1."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 384,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-19T08:39:11.844987Z",
+ "start_time": "2019-10-19T08:39:11.828099Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[1, 2, 3],\n",
+ "\t [4, 5, 6],\n",
+ "\t [7, 8, 9],\n",
+ "\t [10, 11, 12]], dtype=uint8)\n",
+ "shape of a: (4, 3)\n",
+ "\n",
+ "transpose of a:\n",
+ " array([[1, 4, 7, 10],\n",
+ "\t [2, 5, 8, 11],\n",
+ "\t [3, 6, 9, 12]], dtype=uint8)\n",
+ "shape of a: (3, 4)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=np.uint8)\n",
+ "print('a:\\n', a)\n",
+ "print('shape of a:', a.shape)\n",
+ "a.transpose()\n",
+ "print('\\ntranspose of a:\\n', a)\n",
+ "print('shape of a:', a.shape)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The transpose of the array can also be gotten through the `T` property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[1, 2, 3],\n",
+ " [4, 5, 6],\n",
+ " [7, 8, 9]], dtype=uint8)\n",
+ "\n",
+ "transpose of a:\n",
+ " array([[1, 4, 7],\n",
+ " [2, 5, 8],\n",
+ " [3, 6, 9]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)\n",
+ "print('a:\\n', a)\n",
+ "print('\\ntranspose of a:\\n', a.T)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### .sort\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html\n",
+ "\n",
+ "In-place sorting of an `ndarray`. For a more detailed exposition, see [sort](#sort)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:11:20.989109Z",
+ "start_time": "2021-01-13T06:11:20.972842Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[1, 12, 3, 0],\n",
+ " [5, 3, 4, 1],\n",
+ " [9, 11, 1, 8],\n",
+ " [7, 10, 0, 1]], dtype=uint8)\n",
+ "\n",
+ "a sorted along vertical axis:\n",
+ " array([[1, 3, 0, 0],\n",
+ " [5, 10, 1, 1],\n",
+ " [7, 11, 3, 1],\n",
+ " [9, 12, 4, 8]], dtype=uint8)\n",
+ "\n",
+ "a sorted along horizontal axis:\n",
+ " array([[0, 1, 3, 12],\n",
+ " [1, 3, 4, 5],\n",
+ " [1, 8, 9, 11],\n",
+ " [0, 1, 7, 10]], dtype=uint8)\n",
+ "\n",
+ "flattened a sorted:\n",
+ " array([0, 0, 1, ..., 10, 11, 12], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)\n",
+ "print('\\na:\\n', a)\n",
+ "a.sort(axis=0)\n",
+ "print('\\na sorted along vertical axis:\\n', a)\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)\n",
+ "a.sort(axis=1)\n",
+ "print('\\na sorted along horizontal axis:\\n', a)\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)\n",
+ "a.sort(axis=None)\n",
+ "print('\\nflattened a sorted:\\n', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Unary operators\n",
+ "\n",
+ "With the exception of `len`, which returns a single number, all unary operators manipulate the underlying data element-wise. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### len\n",
+ "\n",
+ "This operator takes a single argument, the array, and returns either the length of the first axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:11:49.266192Z",
+ "start_time": "2021-01-13T06:11:49.255493Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([1, 2, 3, 4, 5], dtype=uint8)\n",
+ "length of a: 5\n",
+ "shape of a: (5,)\n",
+ "\n",
+ "b:\t array([[0, 1, 2, 3, 4],\n",
+ " [0, 1, 2, 3, 4],\n",
+ " [0, 1, 2, 3, 4],\n",
+ " [0, 1, 2, 3, 4]], dtype=uint8)\n",
+ "length of b: 2\n",
+ "shape of b: (4, 5)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)\n",
+ "b = np.array([range(5), range(5), range(5), range(5)], dtype=np.uint8)\n",
+ "\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"length of a: \", len(a))\n",
+ "print(\"shape of a: \", a.shape)\n",
+ "print(\"\\nb:\\t\", b)\n",
+ "print(\"length of b: \", len(b))\n",
+ "print(\"shape of b: \", b.shape)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ " The number returned by `len` is also the length of the iterations, when the array supplies the elements for an iteration (see later)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### invert\n",
+ "\n",
+ "The function is defined for integer data types (`uint8`, `int8`, `uint16`, and `int16`) only, takes a single argument, and returns the element-by-element, bit-wise inverse of the array. If a `float` is supplied, the function raises a `ValueError` exception.\n",
+ "\n",
+ "With signed integers (`int8`, and `int16`), the results might be unexpected, as in the example below:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 98,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-11T13:16:16.754210Z",
+ "start_time": "2019-10-11T13:16:16.735618Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([0, -1, -100], dtype=int8)\n",
+ "inverse of a:\t array([-1, 0, 99], dtype=int8)\n",
+ "\n",
+ "a:\t\t array([0, 1, 254, 255], dtype=uint8)\n",
+ "inverse of a:\t array([255, 254, 1, 0], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([0, -1, -100], dtype=np.int8)\n",
+ "print(\"a:\\t\\t\", a)\n",
+ "print(\"inverse of a:\\t\", ~a)\n",
+ "\n",
+ "a = np.array([0, 1, 254, 255], dtype=np.uint8)\n",
+ "print(\"\\na:\\t\\t\", a)\n",
+ "print(\"inverse of a:\\t\", ~a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### abs\n",
+ "\n",
+ "This function takes a single argument, and returns the element-by-element absolute value of the array. When the data type is unsigned (`uint8`, or `uint16`), a copy of the array will be returned immediately, and no calculation takes place."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 73,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-11T13:05:43.926821Z",
+ "start_time": "2019-10-11T13:05:43.912629Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t\t array([0, -1, -100], dtype=int8)\n",
+ "absolute value of a:\t array([0, 1, 100], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([0, -1, -100], dtype=np.int8)\n",
+ "print(\"a:\\t\\t\\t \", a)\n",
+ "print(\"absolute value of a:\\t \", abs(a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### neg\n",
+ "\n",
+ "This operator takes a single argument, and changes the sign of each element in the array. Unsigned values are wrapped. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 99,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-11T13:17:00.946009Z",
+ "start_time": "2019-10-11T13:17:00.927264Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([10, -1, 1], dtype=int8)\n",
+ "negative of a:\t array([-10, 1, -1], dtype=int8)\n",
+ "\n",
+ "b:\t\t array([0, 100, 200], dtype=uint8)\n",
+ "negative of b:\t array([0, 156, 56], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([10, -1, 1], dtype=np.int8)\n",
+ "print(\"a:\\t\\t\", a)\n",
+ "print(\"negative of a:\\t\", -a)\n",
+ "\n",
+ "b = np.array([0, 100, 200], dtype=np.uint8)\n",
+ "print(\"\\nb:\\t\\t\", b)\n",
+ "print(\"negative of b:\\t\", -b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### pos\n",
+ "\n",
+ "This function takes a single argument, and simply returns a copy of the array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 85,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-11T13:09:15.965662Z",
+ "start_time": "2019-10-11T13:09:15.945461Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t array([10, -1, 1], dtype=int8)\n",
+ "positive of a:\t array([10, -1, 1], dtype=int8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([10, -1, 1], dtype=np.int8)\n",
+ "print(\"a:\\t\\t\", a)\n",
+ "print(\"positive of a:\\t\", +a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Binary operators\n",
+ "\n",
+ "`ulab` implements the `+`, `-`, `*`, `/`, `**`, `<`, `>`, `<=`, `>=`, `==`, `!=`, `+=`, `-=`, `*=`, `/=`, `**=` binary operators that work element-wise. Broadcasting is available, meaning that the two operands do not even have to have the same shape. If the lengths along the respective axes are equal, or one of them is 1, or the axis is missing, the element-wise operation can still be carried out. \n",
+ "A thorough explanation of broadcasting can be found under https://numpy.org/doc/stable/user/basics.broadcasting.html. \n",
+ "\n",
+ "**WARNING**: note that relational operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) should have the `ndarray` on their left hand side, when compared to scalars. This means that the following works"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 50,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:12:30.802935Z",
+ "start_time": "2021-01-13T06:12:30.786069Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([False, False, True], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3])\n",
+ "print(a > 2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "while the equivalent statement, `2 < a`, will raise a `TypeError` exception:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:12:51.262197Z",
+ "start_time": "2021-01-13T06:12:51.244206Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "Traceback (most recent call last):\n",
+ " File \"/dev/shm/micropython.py\", line 5, in <module>\n",
+ "TypeError: unsupported types for __lt__: 'int', 'ndarray'\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3])\n",
+ "print(2 < a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**WARNING:** `circuitpython` users should use the `equal`, and `not_equal` operators instead of `==`, and `!=`. See the section on [array comparison](#Comparison-of-arrays) for details."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Upcasting\n",
+ "\n",
+ "Binary operations require special attention, because two arrays with different typecodes can be the operands of an operation, in which case it is not trivial, what the typecode of the result is. This decision on the result's typecode is called upcasting. Since the number of typecodes in `ulab` is significantly smaller than in `numpy`, we have to define new upcasting rules. Where possible, I followed `numpy`'s conventions. \n",
+ "\n",
+ "`ulab` observes the following upcasting rules:\n",
+ "\n",
+ "1. Operations on two `ndarray`s of the same `dtype` preserve their `dtype`, even when the results overflow.\n",
+ "\n",
+ "2. if either of the operands is a float, the result is automatically a float\n",
+ "\n",
+ "3. When one of the operands is a scalar, it will internally be turned into a single-element `ndarray` with the *smallest* possible `dtype`. Thus, e.g., if the scalar is 123, it will be converted into an array of `dtype` `uint8`, while -1000 will be converted into `int16`. An `mp_obj_float`, will always be promoted to `dtype` `float`. Other micropython types (e.g., lists, tuples, etc.) raise a `TypeError` exception. \n",
+ "\n",
+ "4. \n",
+ " \n",
+ "| left hand side | right hand side | ulab result | numpy result |\n",
+ "|----------------|-----------------|-------------|--------------|\n",
+ "|`uint8` |`int8` |`int16` |`int16` |\n",
+ "|`uint8` |`int16` |`int16` |`int16` |\n",
+ "|`uint8` |`uint16` |`uint16` |`uint16` |\n",
+ "|`int8` |`int16` |`int16` |`int16` | \n",
+ "|`int8` |`uint16` |`uint16` |`int32` |\n",
+ "|`uint16` |`int16` |`float` |`int32` |\n",
+ " \n",
+ "Note that the last two operations are promoted to `int32` in `numpy`.\n",
+ " \n",
+ "**WARNING:** Due to the lower number of available data types, the upcasting rules of `ulab` are slightly different to those of `numpy`. Watch out for this, when porting code!\n",
+ "\n",
+ "Upcasting can be seen in action in the following snippet:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 53,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:13:23.026904Z",
+ "start_time": "2021-01-13T06:13:23.009315Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([1, 2, 3, 4], dtype=uint8)\n",
+ "b:\t array([1, 2, 3, 4], dtype=int8)\n",
+ "a+b:\t array([2, 4, 6, 8], dtype=int16)\n",
+ "\n",
+ "a:\t array([1, 2, 3, 4], dtype=uint8)\n",
+ "c:\t array([1.0, 2.0, 3.0, 4.0], dtype=float64)\n",
+ "a*c:\t array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
+ "b = np.array([1, 2, 3, 4], dtype=np.int8)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"b:\\t\", b)\n",
+ "print(\"a+b:\\t\", a+b)\n",
+ "\n",
+ "c = np.array([1, 2, 3, 4], dtype=np.float)\n",
+ "print(\"\\na:\\t\", a)\n",
+ "print(\"c:\\t\", c)\n",
+ "print(\"a*c:\\t\", a*c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Benchmarks\n",
+ "\n",
+ "The following snippet compares the performance of binary operations to a possible implementation in python. For the time measurement, we will take the following snippet from the micropython manual:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T06:39:52.225256Z",
+ "start_time": "2020-05-07T06:39:52.194691Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 490,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-19T13:23:45.432395Z",
+ "start_time": "2019-10-19T13:23:45.344021Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "python add:\n",
+ "execution time: 10051 us\n",
+ "\n",
+ "python multiply:\n",
+ "execution time: 14175 us\n",
+ "\n",
+ "ulab add:\n",
+ "execution time: 222 us\n",
+ "\n",
+ "ulab multiply:\n",
+ "execution time: 213 us\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "@timeit\n",
+ "def py_add(a, b):\n",
+ " return [a[i]+b[i] for i in range(1000)]\n",
+ "\n",
+ "@timeit\n",
+ "def py_multiply(a, b):\n",
+ " return [a[i]*b[i] for i in range(1000)]\n",
+ "\n",
+ "@timeit\n",
+ "def ulab_add(a, b):\n",
+ " return a + b\n",
+ "\n",
+ "@timeit\n",
+ "def ulab_multiply(a, b):\n",
+ " return a * b\n",
+ "\n",
+ "a = [0.0]*1000\n",
+ "b = range(1000)\n",
+ "\n",
+ "print('python add:')\n",
+ "py_add(a, b)\n",
+ "\n",
+ "print('\\npython multiply:')\n",
+ "py_multiply(a, b)\n",
+ "\n",
+ "a = np.linspace(0, 10, num=1000)\n",
+ "b = np.ones(1000)\n",
+ "\n",
+ "print('\\nulab add:')\n",
+ "ulab_add(a, b)\n",
+ "\n",
+ "print('\\nulab multiply:')\n",
+ "ulab_multiply(a, b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The python implementation above is not perfect, and certainly, there is much room for improvement. However, the factor of 50 difference in execution time is very spectacular. This is nothing but a consequence of the fact that the `ulab` functions run `C` code, with very little python overhead. The factor of 50 appears to be quite universal: the FFT routine obeys similar scaling (see [Speed of FFTs](#Speed-of-FFTs)), and this number came up with font rendering, too: [fast font rendering on graphical displays](https://forum.micropython.org/viewtopic.php?f=15&t=5815&p=33362&hilit=ufont#p33383)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Comparison operators\n",
+ "\n",
+ "The smaller than, greater than, smaller or equal, and greater or equal operators return a vector of Booleans indicating the positions (`True`), where the condition is satisfied. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 99,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T15:08:38.673585Z",
+ "start_time": "2020-10-17T15:08:38.659225Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([True, True, True, True, False, False, False, False], dtype=bool)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint8)\n",
+ "print(a < 5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**WARNING**: at the moment, due to `micropython`'s implementation details, the `ndarray` must be on the left hand side of the relational operators.\n",
+ "\n",
+ "That is, while `a < 5` and `5 > a` have the same meaning, the following code will not work:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "Traceback (most recent call last):\n",
+ " File \"/dev/shm/micropython.py\", line 5, in <module>\n",
+ "TypeError: unsupported types for __gt__: 'int', 'ndarray'\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint8)\n",
+ "print(5 > a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Iterating over arrays\n",
+ "\n",
+ "`ndarray`s are iterable, which means that their elements can also be accessed as can the elements of a list, tuple, etc. If the array is one-dimensional, the iterator returns scalars, otherwise a new reduced-dimensional *view* is created and returned."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 54,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-01-13T06:14:11.756254Z",
+ "start_time": "2021-01-13T06:14:11.742246Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([1, 2, 3, 4, 5], dtype=uint8)\n",
+ "element 0 in a: 1\n",
+ "element 1 in a: 2\n",
+ "element 2 in a: 3\n",
+ "element 3 in a: 4\n",
+ "element 4 in a: 5\n",
+ "\n",
+ "b:\t array([[0, 1, 2, 3, 4],\n",
+ " [10, 11, 12, 13, 14],\n",
+ " [20, 21, 22, 23, 24],\n",
+ " [30, 31, 32, 33, 34]], dtype=uint8)\n",
+ "element 0 in b: array([0, 1, 2, 3, 4], dtype=uint8)\n",
+ "element 1 in b: array([10, 11, 12, 13, 14], dtype=uint8)\n",
+ "element 2 in b: array([20, 21, 22, 23, 24], dtype=uint8)\n",
+ "element 3 in b: array([30, 31, 32, 33, 34], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)\n",
+ "b = np.array([range(5), range(10, 15, 1), range(20, 25, 1), range(30, 35, 1)], dtype=np.uint8)\n",
+ "\n",
+ "print(\"a:\\t\", a)\n",
+ "\n",
+ "for i, _a in enumerate(a):\n",
+ " print(\"element %d in a:\"%i, _a)\n",
+ " \n",
+ "print(\"\\nb:\\t\", b)\n",
+ "\n",
+ "for i, _b in enumerate(b):\n",
+ " print(\"element %d in b:\"%i, _b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Slicing and indexing\n",
+ "\n",
+ "\n",
+ "### Views vs. copies\n",
+ "\n",
+ "`numpy` has a very important concept called *views*, which is a powerful extension of `python`'s own notion of slicing. Slices are special python objects of the form\n",
+ "\n",
+ "```python\n",
+ "slice = start:end:stop\n",
+ "```\n",
+ "\n",
+ "where `start`, `end`, and `stop` are (not necessarily non-negative) integers. Not all of these three numbers must be specified in an index, in fact, all three of them can be missing. The interpreter takes care of filling in the missing values. (Note that slices cannot be defined in this way, only there, where an index is expected.) For a good explanation on how slices work in python, you can read the stackoverflow question https://stackoverflow.com/questions/509211/understanding-slice-notation.\n",
+ "\n",
+ "In order to see what slicing does, let us take the string `a = '012345679'`! We can extract every second character by creating the slice `::2`, which is equivalent to `0:len(a):2`, i.e., increments the character pointer by 2 starting from 0, and traversing the string up to the very end."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T05:26:17.758735Z",
+ "start_time": "2020-10-12T05:26:17.748487Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'02468'"
+ ]
+ },
+ "execution_count": 23,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "string = '0123456789'\n",
+ "string[::2]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, we can do the same with numerical arrays."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T05:25:49.352435Z",
+ "start_time": "2020-10-12T05:25:49.339452Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)\n",
+ "a[::2]:\t array([0, 2, 4, 6, 8], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(10), dtype=np.uint8)\n",
+ "print('a:\\t', a)\n",
+ "\n",
+ "print('a[::2]:\\t', a[::2])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This looks similar to `string` above, but there is a very important difference that is not so obvious. Namely, `string[::2]` produces a partial copy of `string`, while `a[::2]` only produces a *view* of `a`. What this means is that `a`, and `a[::2]` share their data, and the only difference between the two is, how the data are read out. In other words, internally, `a[::2]` has the same data pointer as `a`. We can easily convince ourselves that this is indeed the case by calling the [ndinfo](#The_ndinfo_function) function: the *data pointer* entry is the same in the two printouts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 73,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-16T18:43:07.480791Z",
+ "start_time": "2020-10-16T18:43:07.471473Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8) \n",
+ "\n",
+ "class: ndarray\n",
+ "shape: (10,)\n",
+ "strides: (1,)\n",
+ "itemsize: 1\n",
+ "data pointer: 0x7ff6c6193220\n",
+ "type: uint8\n",
+ "\n",
+ "====================\n",
+ "a[::2]: array([0, 2, 4, 6, 8], dtype=uint8) \n",
+ "\n",
+ "class: ndarray\n",
+ "shape: (5,)\n",
+ "strides: (2,)\n",
+ "itemsize: 1\n",
+ "data pointer: 0x7ff6c6193220\n",
+ "type: uint8\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(10), dtype=np.uint8)\n",
+ "print('a: ', a, '\\n')\n",
+ "np.ndinfo(a)\n",
+ "print('\\n' + '='*20)\n",
+ "print('a[::2]: ', a[::2], '\\n')\n",
+ "np.ndinfo(a[::2])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you are still a bit confused about the meaning of *views*, the section [Slicing and assigning to slices](#Slicing-and-assigning-to-slices) should clarify the issue."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Indexing\n",
+ "\n",
+ "The simplest form of indexing is specifying a single integer between the square brackets as in "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 64,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T18:31:45.485584Z",
+ "start_time": "2020-10-12T18:31:45.464551Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)\n",
+ "the first, and last element of a:\n",
+ " 0 9\n",
+ "the second, and last but one element of a:\n",
+ " 1 8\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(10), dtype=np.uint8)\n",
+ "print(\"a: \", a)\n",
+ "print(\"the first, and last element of a:\\n\", a[0], a[-1])\n",
+ "print(\"the second, and last but one element of a:\\n\", a[1], a[-2])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Indexing can be applied to higher-dimensional tensors, too. When the length of the indexing sequences is smaller than the number of dimensions, a new *view* is returned, otherwise, we get a single number."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 60,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T18:26:12.783883Z",
+ "start_time": "2020-10-12T18:26:12.770180Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[0, 1, 2],\n",
+ "\t[3, 4, 5],\n",
+ "\t[6, 7, 8]], dtype=uint8)\n",
+ "a[0]:\n",
+ " array([[0, 1, 2]], dtype=uint8)\n",
+ "a[1,1]: 4\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8).reshape((3, 3))\n",
+ "print(\"a:\\n\", a)\n",
+ "print(\"a[0]:\\n\", a[0])\n",
+ "print(\"a[1,1]: \", a[1,1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Indices can also be a list of Booleans. By using a Boolean list, we can select those elements of an array that satisfy a specific condition. At the moment, such indexing is defined for row vectors only; when the rank of the tensor is higher than 1, the function raises a `NotImplementedError` exception, though this will be rectified in a future version of `ulab`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T17:34:34.105614Z",
+ "start_time": "2020-10-12T17:34:34.094017Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
+ "a < 5:\t array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.float)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"a < 5:\\t\", a[a < 5])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Indexing with Boolean arrays can take more complicated expressions. This is a very concise way of comparing two vectors, e.g.:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T18:03:38.846377Z",
+ "start_time": "2020-10-12T18:03:38.826689Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "\n",
+ "a**2:\t array([0, 1, 4, 9, 16, 25, 36, 49, 64], dtype=uint16)\n",
+ "\n",
+ "b:\t array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=uint8)\n",
+ "\n",
+ "100*sin(b):\t array([-75.68024953079282, -75.68024953079282, -75.68024953079282, 14.11200080598672, 14.11200080598672, 14.11200080598672, 42.01670368266409, 42.01670368266409, 42.01670368266409], dtype=float)\n",
+ "\n",
+ "a[a*a > np.sin(b)*100.0]:\t array([0, 1, 2, 4, 5, 7, 8], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)\n",
+ "print(\"a:\\t\", a)\n",
+ "print(\"\\na**2:\\t\", a*a)\n",
+ "print(\"\\nb:\\t\", b)\n",
+ "print(\"\\n100*sin(b):\\t\", np.sin(b)*100.0)\n",
+ "print(\"\\na[a*a > np.sin(b)*100.0]:\\t\", a[a*a > np.sin(b)*100.0])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Boolean indices can also be used in assignments, if the array is one-dimensional. The following example replaces the data in an array, wherever some condition is fulfilled."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 72,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-13T16:14:21.055356Z",
+ "start_time": "2020-10-13T16:14:21.035329Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([0, 1, 2], dtype=uint8)\n",
+ "array([123, 123, 123, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "b = np.array(range(9)) + 12\n",
+ "\n",
+ "print(a[b < 15])\n",
+ "\n",
+ "a[b < 15] = 123\n",
+ "print(a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "On the right hand side of the assignment we can even have another array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 71,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-13T16:14:10.054210Z",
+ "start_time": "2020-10-13T16:14:10.039523Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([0, 1, 2], dtype=uint8) array([12.0, 13.0, 14.0], dtype=float)\n",
+ "array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "b = np.array(range(9)) + 12\n",
+ "\n",
+ "print(a[b < 15], b[b < 15])\n",
+ "\n",
+ "a[b < 15] = b[b < 15]\n",
+ "print(a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Slicing and assigning to slices\n",
+ "\n",
+ "You can also generate sub-arrays by specifying slices as the index of an array. Slices are special python objects of the form "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T17:38:15.975404Z",
+ "start_time": "2020-10-12T17:38:15.955070Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[1, 2, 3],\n",
+ "\t[4, 5, 6],\n",
+ "\t[7, 8, 9]], dtype=uint8)\n",
+ "\n",
+ "a[0]:\n",
+ " array([[1, 2, 3]], dtype=uint8)\n",
+ "\n",
+ "a[0,:2]:\n",
+ " array([[1, 2]], dtype=uint8)\n",
+ "\n",
+ "a[:,0]:\n",
+ " array([[1],\n",
+ "\t[4],\n",
+ "\t[7]], dtype=uint8)\n",
+ "\n",
+ "a[-1]:\n",
+ " array([[7, 8, 9]], dtype=uint8)\n",
+ "\n",
+ "a[-1:-3:-1]:\n",
+ " array([[7, 8, 9],\n",
+ "\t[4, 5, 6]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)\n",
+ "print('a:\\n', a)\n",
+ "\n",
+ "# the first row\n",
+ "print('\\na[0]:\\n', a[0])\n",
+ "\n",
+ "# the first two elements of the first row\n",
+ "print('\\na[0,:2]:\\n', a[0,:2])\n",
+ "\n",
+ "# the zeroth element in each row (also known as the zeroth column)\n",
+ "print('\\na[:,0]:\\n', a[:,0])\n",
+ "\n",
+ "# the last row\n",
+ "print('\\na[-1]:\\n', a[-1])\n",
+ "\n",
+ "# the last two rows backwards\n",
+ "print('\\na[-1:-3:-1]:\\n', a[-1:-3:-1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Assignment to slices can be done for the whole slice, per row, and per column. A couple of examples should make these statements clearer:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T17:40:24.031254Z",
+ "start_time": "2020-10-12T17:40:24.011816Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[0, 0, 0],\n",
+ "\t[0, 0, 0],\n",
+ "\t[0, 0, 0]], dtype=uint8)\n",
+ "\n",
+ "a[0] = 1\n",
+ " array([[1, 1, 1],\n",
+ "\t[0, 0, 0],\n",
+ "\t[0, 0, 0]], dtype=uint8)\n",
+ "\n",
+ "a[:,0]:\n",
+ " array([[0, 0, 3],\n",
+ "\t[0, 0, 3],\n",
+ "\t[0, 0, 3]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.zeros((3, 3), dtype=np.uint8)\n",
+ "print('a:\\n', a)\n",
+ "\n",
+ "# assigning to the whole row\n",
+ "a[0] = 1\n",
+ "print('\\na[0] = 1\\n', a)\n",
+ "\n",
+ "a = np.zeros((3, 3), dtype=np.uint8)\n",
+ "\n",
+ "# assigning to a column\n",
+ "a[:,2] = 3.0\n",
+ "print('\\na[:,0]:\\n', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, you should notice that we re-set the array `a` after the first assignment. Do you care to see what happens, if we do not do that? Well, here are the results:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-12T17:44:09.180623Z",
+ "start_time": "2020-10-12T17:44:09.161578Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([[1, 1, 3],\n",
+ "\t[0, 0, 3],\n",
+ "\t[0, 0, 3]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.zeros((3, 3), dtype=np.uint8)\n",
+ "b = a[:,:]\n",
+ "# assign 1 to the first row\n",
+ "b[0] = 1\n",
+ "\n",
+ "# assigning to the last column\n",
+ "b[:,2] = 3\n",
+ "print('a: ', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that both assignments involved `b`, and not `a`, yet, when we print out `a`, its entries are updated. This proves our earlier statement about the behaviour of *views*: in the statement `b = a[:,:]` we simply created a *view* of `a`, and not a *deep* copy of it, meaning that whenever we modify `b`, we actually modify `a`, because the underlying data container of `a` and `b` are shared between the two object. Having a single data container for two seemingly different objects provides an extremely powerful way of manipulating sub-sets of numerical data."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you want to work on a *copy* of your data, you can use the `.copy` method of the `ndarray`. The following snippet should drive the point home:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 90,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T13:06:20.223171Z",
+ "start_time": "2020-10-17T13:06:20.206422Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "class: ndarray\n",
+ "shape: (3, 3)\n",
+ "strides: (3, 1)\n",
+ "itemsize: 1\n",
+ "data pointer: 0x7ff737ea3220\n",
+ "type: uint8\n",
+ "\n",
+ "class: ndarray\n",
+ "shape: (3, 3)\n",
+ "strides: (3, 1)\n",
+ "itemsize: 1\n",
+ "data pointer: 0x7ff737ea3340\n",
+ "type: uint8\n",
+ "\n",
+ "a: array([[0, 0, 0],\n",
+ "\t[0, 0, 0],\n",
+ "\t[0, 0, 0]], dtype=uint8)\n",
+ "====================\n",
+ "b: array([[1, 1, 1],\n",
+ "\t[0, 0, 0],\n",
+ "\t[0, 0, 0]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.zeros((3, 3), dtype=np.uint8)\n",
+ "b = a.copy()\n",
+ "\n",
+ "# get the address of the underlying data pointer\n",
+ "\n",
+ "np.ndinfo(a)\n",
+ "print()\n",
+ "np.ndinfo(b)\n",
+ "\n",
+ "# assign 1 to the first row of b, and do not touch a\n",
+ "b[0] = 1\n",
+ "\n",
+ "print()\n",
+ "print('a: ', a)\n",
+ "print('='*20)\n",
+ "print('b: ', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `.copy` method can also be applied to views: below, `a[0]` is a *view* of `a`, out of which we create a *deep copy* called `b`. This is a row vector now. We can then do whatever we want to with `b`, and that leaves `a` unchanged."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 85,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T13:00:06.217232Z",
+ "start_time": "2020-10-17T13:00:06.207417Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "b: array([0, 0, 0], dtype=uint8)\n",
+ "====================\n",
+ "a: array([[0, 0, 0],\n",
+ "\t[0, 0, 0],\n",
+ "\t[0, 0, 0]], dtype=uint8)\n",
+ "====================\n",
+ "b: array([1, 0, 0], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.zeros((3, 3), dtype=np.uint8)\n",
+ "b = a[0].copy()\n",
+ "print('b: ', b)\n",
+ "print('='*20)\n",
+ "# assign 1 to the first entry of b, and do not touch a\n",
+ "b[0] = 1\n",
+ "print('a: ', a)\n",
+ "print('='*20)\n",
+ "print('b: ', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The fact that the underlying data of a view is the same as that of the original array has another important consequence, namely, that the creation of a view is cheap. Both in terms of RAM, and execution time. A view is really nothing but a short header with a data array that already exists, and is filled up. Hence, creating the view requires only the creation of its header. This operation is fast, and uses virtually no RAM."
+ ]
+ }
+ ],
+ "metadata": {
+ "interpreter": {
+ "hash": "ce9a02f9f7db620716422019cafa4bc1786ca85daa298b819f6da075e7993842"
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-numerical.ipynb b/circuitpython/extmod/ulab/docs/ulab-numerical.ipynb
new file mode 100644
index 0000000..be54954
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-numerical.ipynb
@@ -0,0 +1,1160 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-11-03T19:50:50.150235Z",
+ "start_time": "2020-11-03T19:50:48.888079Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:16:29.118001Z",
+ "start_time": "2022-01-07T19:16:29.114692Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2022-01-07T19:16:37.453883Z",
+ "start_time": "2022-01-07T19:16:37.422478Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Numerical\n",
+ "\n",
+ "Function in this section can be used for calculating statistical properties, or manipulating the arrangement of array elements."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## min, argmin, max, argmax\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.min.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.max.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
+ "\n",
+ "**WARNING:** Difference to `numpy`: the `out` keyword argument is not implemented.\n",
+ "\n",
+ "These functions follow the same pattern, and work with generic iterables, and `ndarray`s. `min`, and `max` return the minimum or maximum of a sequence. If the input array is two-dimensional, the `axis` keyword argument can be supplied, in which case the minimum/maximum along the given axis will be returned. If `axis=None` (this is also the default value), the minimum/maximum of the flattened array will be determined.\n",
+ "\n",
+ "`argmin/argmax` return the position (index) of the minimum/maximum in the sequence."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 108,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T21:26:22.507996Z",
+ "start_time": "2020-10-17T21:26:22.492543Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([1.0, 2.0, 3.0], dtype=float)\n",
+ "array([], dtype=float)\n",
+ "[] 0\n",
+ "array([1.0, 2.0, 3.0], dtype=float)\n",
+ "array([], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "\n",
+ "a = np.array([1, 2, 3])\n",
+ "print(a)\n",
+ "print(a[-1:-1:-3])\n",
+ "try:\n",
+ " sa = list(a[-1:-1:-3])\n",
+ " la = len(sa)\n",
+ "except IndexError as e:\n",
+ " sa = str(e)\n",
+ " la = -1\n",
+ " \n",
+ "print(sa, la)\n",
+ "\n",
+ "a[-1:-1:-3] = np.ones(0)\n",
+ "print(a)\n",
+ "\n",
+ "b = np.ones(0) + 1\n",
+ "print(b)\n",
+ "# print('b', b.shape())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 122,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T21:54:49.123748Z",
+ "start_time": "2020-10-17T21:54:49.093819Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0, 1, -3array([], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "a = np.array([1, 2, 3])\n",
+ "print(a[0:1:-3])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 127,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T21:57:01.482277Z",
+ "start_time": "2020-10-17T21:57:01.477362Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0]"
+ ]
+ },
+ "execution_count": 127,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "l = list(range(13))\n",
+ "\n",
+ "l[0:10:113]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 81,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-17T20:59:58.285134Z",
+ "start_time": "2020-10-17T20:59:58.263605Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0,)"
+ ]
+ },
+ "execution_count": 81,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "a = np.array([1, 2, 3])\n",
+ "np.ones(0, dtype=uint8) / np.zeros(0, dtype=uint16)\n",
+ "np.ones(0).shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 375,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-18T13:08:28.113525Z",
+ "start_time": "2019-10-18T13:08:28.093518Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: array([1.0, 2.0, 0.0, 1.0, 10.0], dtype=float)\n",
+ "min of a: 0.0\n",
+ "argmin of a: 2\n",
+ "\n",
+ "b:\n",
+ " array([[1.0, 2.0, 0.0],\n",
+ "\t [1.0, 10.0, -1.0]], dtype=float)\n",
+ "min of b (flattened): -1.0\n",
+ "min of b (axis=0): array([1.0, 2.0, -1.0], dtype=float)\n",
+ "min of b (axis=1): array([0.0, -1.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([1, 2, 0, 1, 10])\n",
+ "print('a:', a)\n",
+ "print('min of a:', numerical.min(a))\n",
+ "print('argmin of a:', numerical.argmin(a))\n",
+ "\n",
+ "b = np.array([[1, 2, 0], [1, 10, -1]])\n",
+ "print('\\nb:\\n', b)\n",
+ "print('min of b (flattened):', numerical.min(b))\n",
+ "print('min of b (axis=0):', numerical.min(b, axis=0))\n",
+ "print('min of b (axis=1):', numerical.min(b, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sum, std, mean\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sum.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.std.html\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.mean.html\n",
+ "\n",
+ "These three functions follow the same pattern: if the axis keyword is not specified, it assumes the default value of `None`, and returns the result of the computation for the flattened array. Otherwise, the calculation is along the given axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 527,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-20T06:51:58.845076Z",
+ "start_time": "2019-10-20T06:51:58.798730Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \n",
+ " array([[1.0, 2.0, 3.0],\n",
+ "\t [4.0, 5.0, 6.0],\n",
+ "\t [7.0, 8.0, 9.0]], dtype=float)\n",
+ "sum, flat array: 45.0\n",
+ "mean, horizontal: array([2.0, 5.0, 8.0], dtype=float)\n",
+ "std, vertical: array([2.44949, 2.44949, 2.44949], dtype=float)\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
+ "print('a: \\n', a)\n",
+ "\n",
+ "print('sum, flat array: ', numerical.sum(a))\n",
+ "\n",
+ "print('mean, horizontal: ', numerical.mean(a, axis=1))\n",
+ "\n",
+ "print('std, vertical: ', numerical.std(a, axis=0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## roll\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.roll.html\n",
+ "\n",
+ "The roll function shifts the content of a vector by the positions given as the second argument. If the `axis` keyword is supplied, the shift is applied to the given axis."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 229,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-11T19:39:47.459395Z",
+ "start_time": "2019-10-11T19:39:47.443691Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\t\t\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
+ "a rolled to the left:\t array([3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0], dtype=float)\n",
+ "a rolled to the right:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5, 6, 7, 8])\n",
+ "print(\"a:\\t\\t\\t\", a)\n",
+ "\n",
+ "numerical.roll(a, 2)\n",
+ "print(\"a rolled to the left:\\t\", a)\n",
+ "\n",
+ "# this should be the original vector\n",
+ "numerical.roll(a, -2)\n",
+ "print(\"a rolled to the right:\\t\", a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Rolling works with matrices, too. If the `axis` keyword is 0, the matrix is rolled along its vertical axis, otherwise, horizontally. \n",
+ "\n",
+ "Horizontal rolls are faster, because they require fewer steps, and larger memory chunks are copied, however, they also require more RAM: basically the whole row must be stored internally. Most expensive are the `None` keyword values, because with `axis = None`, the array is flattened first, hence the row's length is the size of the whole matrix.\n",
+ "\n",
+ "Vertical rolls require two internal copies of single columns. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 268,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-15T17:46:20.051069Z",
+ "start_time": "2019-10-15T17:46:20.033205Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[1.0, 2.0, 3.0, 4.0],\n",
+ "\t [5.0, 6.0, 7.0, 8.0]], dtype=float)\n",
+ "\n",
+ "a rolled to the left:\n",
+ " array([[3.0, 4.0, 5.0, 6.0],\n",
+ "\t [7.0, 8.0, 1.0, 2.0]], dtype=float)\n",
+ "\n",
+ "a rolled up:\n",
+ " array([[6.0, 3.0, 4.0, 5.0],\n",
+ "\t [2.0, 7.0, 8.0, 1.0]], dtype=float)\n",
+ "\n",
+ "a rolled with None:\n",
+ " array([[3.0, 4.0, 5.0, 2.0],\n",
+ "\t [7.0, 8.0, 1.0, 6.0]], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])\n",
+ "print(\"a:\\n\", a)\n",
+ "\n",
+ "numerical.roll(a, 2)\n",
+ "print(\"\\na rolled to the left:\\n\", a)\n",
+ "\n",
+ "numerical.roll(a, -1, axis=1)\n",
+ "print(\"\\na rolled up:\\n\", a)\n",
+ "\n",
+ "numerical.roll(a, 1, axis=None)\n",
+ "print(\"\\na rolled with None:\\n\", a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Simple running weighted average\n",
+ "\n",
+ "As a demonstration of the conciseness of `ulab/numpy` operations, we will calculate an exponentially weighted running average of a measurement vector in just a couple of lines. I chose this particular example, because I think that this can indeed be used in real-life applications."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 230,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-11T20:03:00.713235Z",
+ "start_time": "2019-10-11T20:03:00.696932Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "array([0.01165623031556606, 0.03168492019176483, 0.08612854033708572, 0.234121635556221, 0.6364086270332336], dtype=float)\n",
+ "0.2545634508132935\n",
+ "array([0.0, 0.0, 0.0, 0.0, 2.0], dtype=float)\n",
+ "0.3482121050357819\n",
+ "array([0.0, 0.0, 0.0, 2.0, 2.0], dtype=float)\n",
+ "0.3826635211706161\n",
+ "array([0.0, 0.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3953374892473221\n",
+ "array([0.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3999999813735485\n",
+ "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3999999813735485\n",
+ "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3999999813735485\n",
+ "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3999999813735485\n",
+ "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3999999813735485\n",
+ "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "0.3999999813735485\n",
+ "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "from ulab import vector\n",
+ "\n",
+ "def dummy_adc():\n",
+ " # dummy adc function, so that the results are reproducible\n",
+ " return 2\n",
+ " \n",
+ "n = 10\n",
+ "# These are the normalised weights; the last entry is the most dominant\n",
+ "weight = vector.exp([1, 2, 3, 4, 5])\n",
+ "weight = weight/numerical.sum(weight)\n",
+ "\n",
+ "print(weight)\n",
+ "# initial array of samples\n",
+ "samples = np.array([0]*n)\n",
+ "\n",
+ "for i in range(n):\n",
+ " # a new datum is inserted on the right hand side. This simply overwrites whatever was in the last slot\n",
+ " samples[-1] = dummy_adc()\n",
+ " print(numerical.mean(samples[-5:]*weight))\n",
+ " print(samples[-5:])\n",
+ " # the data are shifted by one position to the left\n",
+ " numerical.roll(samples, 1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## flip\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html\n",
+ "\n",
+ "The `flip` function takes one positional, an `ndarray`, and one keyword argument, `axis = None`, and reverses the order of elements along the given axis. If the keyword argument is `None`, the matrix' entries are flipped along all axes. `flip` returns a new copy of the array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 275,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-16T06:35:52.163725Z",
+ "start_time": "2019-10-16T06:35:52.149231Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: \t array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=float)\n",
+ "a flipped:\t array([5.0, 4.0, 3.0, 2.0, 1.0], dtype=float)\n",
+ "\n",
+ "a flipped horizontally\n",
+ " array([[3, 2, 1],\n",
+ "\t [6, 5, 4],\n",
+ "\t [9, 8, 7]], dtype=uint8)\n",
+ "\n",
+ "a flipped vertically\n",
+ " array([[7, 8, 9],\n",
+ "\t [4, 5, 6],\n",
+ "\t [1, 2, 3]], dtype=uint8)\n",
+ "\n",
+ "a flipped horizontally+vertically\n",
+ " array([[9, 8, 7],\n",
+ "\t [6, 5, 4],\n",
+ "\t [3, 2, 1]], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5])\n",
+ "print(\"a: \\t\", a)\n",
+ "print(\"a flipped:\\t\", np.flip(a))\n",
+ "\n",
+ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)\n",
+ "print(\"\\na flipped horizontally\\n\", numerical.flip(a, axis=1))\n",
+ "print(\"\\na flipped vertically\\n\", numerical.flip(a, axis=0))\n",
+ "print(\"\\na flipped horizontally+vertically\\n\", numerical.flip(a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## diff\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.diff.html\n",
+ "\n",
+ "The `diff` function returns the numerical derivative of the forward scheme, or more accurately, the differences of an `ndarray` along a given axis. The order of derivative can be stipulated with the `n` keyword argument, which should be between 0, and 9. Default is 1. If higher order derivatives are required, they can be gotten by repeated calls to the function. The `axis` keyword argument should be -1 (last axis, in `ulab` equivalent to the second axis, and this also happens to be the default value), 0, or 1. \n",
+ "\n",
+ "Beyond the output array, the function requires only a couple of bytes of extra RAM for the differentiation stencil. (The stencil is an `int8` array, one byte longer than `n`. This also explains, why the highest order is 9: the coefficients of a ninth-order stencil all fit in signed bytes, while 10 would require `int16`.) Note that as usual in numerical differentiation (and also in `numpy`), the length of the respective axis will be reduced by `n` after the operation. If `n` is larger than, or equal to the length of the axis, an empty array will be returned.\n",
+ "\n",
+ "**WARNING**: the `diff` function does not implement the `prepend` and `append` keywords that can be found in `numpy`. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 169,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-31T11:51:02.854338Z",
+ "start_time": "2019-10-31T11:51:02.838000Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
+ "\n",
+ "first derivative:\n",
+ " array([1, 1, 1, 1, 1, 1, 1, 1], dtype=uint8)\n",
+ "\n",
+ "second derivative:\n",
+ " array([0, 0, 0, 0, 0, 0, 0], dtype=uint8)\n",
+ "\n",
+ "c:\n",
+ " array([[1.0, 2.0, 3.0, 4.0],\n",
+ "\t [4.0, 3.0, 2.0, 1.0],\n",
+ "\t [1.0, 4.0, 9.0, 16.0],\n",
+ "\t [0.0, 0.0, 0.0, 0.0]], dtype=float)\n",
+ "\n",
+ "first derivative, first axis:\n",
+ " array([[3.0, 1.0, -1.0, -3.0],\n",
+ "\t [-3.0, 1.0, 7.0, 15.0],\n",
+ "\t [-1.0, -4.0, -9.0, -16.0]], dtype=float)\n",
+ "\n",
+ "first derivative, second axis:\n",
+ " array([[1.0, 1.0, 1.0],\n",
+ "\t [-1.0, -1.0, -1.0],\n",
+ "\t [3.0, 5.0, 7.0],\n",
+ "\t [0.0, 0.0, 0.0]], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array(range(9), dtype=np.uint8)\n",
+ "print('a:\\n', a)\n",
+ "\n",
+ "print('\\nfirst derivative:\\n', numerical.diff(a, n=1))\n",
+ "print('\\nsecond derivative:\\n', numerical.diff(a, n=2))\n",
+ "\n",
+ "c = np.array([[1, 2, 3, 4], [4, 3, 2, 1], [1, 4, 9, 16], [0, 0, 0, 0]])\n",
+ "print('\\nc:\\n', c)\n",
+ "print('\\nfirst derivative, first axis:\\n', numerical.diff(c, axis=0))\n",
+ "print('\\nfirst derivative, second axis:\\n', numerical.diff(c, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## median\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.median.html\n",
+ "\n",
+ "The function computes the median along the specified axis, and returns the median of the array elements. If the `axis` keyword argument is `None`, the arrays is flattened first. The `dtype` of the results is always float."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-11-03T19:54:38.047790Z",
+ "start_time": "2020-11-03T19:54:38.029264Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a:\n",
+ " array([[0, 1, 2, 3],\n",
+ " [4, 5, 6, 7],\n",
+ " [8, 9, 10, 11]], dtype=int8)\n",
+ "\n",
+ "median of the flattened array: 5.5\n",
+ "\n",
+ "median along the vertical axis: array([4.0, 5.0, 6.0, 7.0], dtype=float)\n",
+ "\n",
+ "median along the horizontal axis: array([1.5, 5.5, 9.5], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "\n",
+ "a = np.array(range(12), dtype=np.int8).reshape((3, 4))\n",
+ "print('a:\\n', a)\n",
+ "print('\\nmedian of the flattened array: ', np.median(a))\n",
+ "print('\\nmedian along the vertical axis: ', np.median(a, axis=0))\n",
+ "print('\\nmedian along the horizontal axis: ', np.median(a, axis=1))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sort\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html\n",
+ "\n",
+ "The sort function takes an ndarray, and sorts its elements in ascending order along the specified axis using a heap sort algorithm. As opposed to the `.sort()` method discussed earlier, this function creates a copy of its input before sorting, and at the end, returns this copy. Sorting takes place in place, without auxiliary storage. The `axis` keyword argument takes on the possible values of -1 (the last axis, in `ulab` equivalent to the second axis, and this also happens to be the default value), 0, 1, or `None`. The first three cases are identical to those in [diff](#diff), while the last one flattens the array before sorting. \n",
+ "\n",
+ "If descending order is required, the result can simply be `flip`ped, see [flip](#flip).\n",
+ "\n",
+ "**WARNING:** `numpy` defines the `kind`, and `order` keyword arguments that are not implemented here. The function in `ulab` always uses heap sort, and since `ulab` does not have the concept of data fields, the `order` keyword argument would have no meaning."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-11-05T16:06:27.536193Z",
+ "start_time": "2019-11-05T16:06:27.521792Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[1.0, 12.0, 3.0, 0.0],\n",
+ "\t [5.0, 3.0, 4.0, 1.0],\n",
+ "\t [9.0, 11.0, 1.0, 8.0],\n",
+ "\t [7.0, 10.0, 0.0, 1.0]], dtype=float)\n",
+ "\n",
+ "a sorted along vertical axis:\n",
+ " array([[1.0, 3.0, 0.0, 0.0],\n",
+ "\t [5.0, 10.0, 1.0, 1.0],\n",
+ "\t [7.0, 11.0, 3.0, 1.0],\n",
+ "\t [9.0, 12.0, 4.0, 8.0]], dtype=float)\n",
+ "\n",
+ "a sorted along horizontal axis:\n",
+ " array([[0.0, 1.0, 3.0, 12.0],\n",
+ "\t [1.0, 3.0, 4.0, 5.0],\n",
+ "\t [1.0, 8.0, 9.0, 11.0],\n",
+ "\t [0.0, 1.0, 7.0, 10.0]], dtype=float)\n",
+ "\n",
+ "flattened a sorted:\n",
+ " array([0.0, 0.0, 1.0, ..., 10.0, 11.0, 12.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)\n",
+ "print('\\na:\\n', a)\n",
+ "b = numerical.sort(a, axis=0)\n",
+ "print('\\na sorted along vertical axis:\\n', b)\n",
+ "\n",
+ "c = numerical.sort(a, axis=1)\n",
+ "print('\\na sorted along horizontal axis:\\n', c)\n",
+ "\n",
+ "c = numerical.sort(a, axis=None)\n",
+ "print('\\nflattened a sorted:\\n', c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Heap sort requires $\\sim N\\log N$ operations, and notably, the worst case costs only 20% more time than the average. In order to get an order-of-magnitude estimate, we will take the sine of 1000 uniformly spaced numbers between 0, and two pi, and sort them:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import vector\n",
+ "from ulab import numerical\n",
+ "\n",
+ "@timeit\n",
+ "def sort_time(array):\n",
+ " return numerical.sort(array)\n",
+ "\n",
+ "b = vector.sin(np.linspace(0, 6.28, num=1000))\n",
+ "print('b: ', b)\n",
+ "sort_time(b)\n",
+ "print('\\nb sorted:\\n', b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## argsort\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argsort.html\n",
+ "\n",
+ "Similarly to [sort](#sort), `argsort` takes a positional, and a keyword argument, and returns an unsigned short index array of type `ndarray` with the same dimensions as the input, or, if `axis=None`, as a row vector with length equal to the number of elements in the input (i.e., the flattened array). The indices in the output sort the input in ascending order. The routine in `argsort` is the same as in `sort`, therefore, the comments on computational expenses (time and RAM) also apply. In particular, since no copy of the original data is required, virtually no RAM beyond the output array is used. \n",
+ "\n",
+ "Since the underlying container of the output array is of type `uint16_t`, neither of the output dimensions should be larger than 65535. If that happens to be the case, the function will bail out with a `ValueError`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-11-06T06:28:45.719578Z",
+ "start_time": "2019-11-06T06:28:45.704072Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([[1.0, 12.0, 3.0, 0.0],\n",
+ "\t [5.0, 3.0, 4.0, 1.0],\n",
+ "\t [9.0, 11.0, 1.0, 8.0],\n",
+ "\t [7.0, 10.0, 0.0, 1.0]], dtype=float)\n",
+ "\n",
+ "a sorted along vertical axis:\n",
+ " array([[0, 1, 3, 0],\n",
+ "\t [1, 3, 2, 1],\n",
+ "\t [3, 2, 0, 3],\n",
+ "\t [2, 0, 1, 2]], dtype=uint16)\n",
+ "\n",
+ "a sorted along horizontal axis:\n",
+ " array([[3, 0, 2, 1],\n",
+ "\t [3, 1, 2, 0],\n",
+ "\t [2, 3, 0, 1],\n",
+ "\t [2, 3, 0, 1]], dtype=uint16)\n",
+ "\n",
+ "flattened a sorted:\n",
+ " array([3, 14, 0, ..., 13, 9, 1], dtype=uint16)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)\n",
+ "print('\\na:\\n', a)\n",
+ "b = numerical.argsort(a, axis=0)\n",
+ "print('\\na sorted along vertical axis:\\n', b)\n",
+ "\n",
+ "c = numerical.argsort(a, axis=1)\n",
+ "print('\\na sorted along horizontal axis:\\n', c)\n",
+ "\n",
+ "c = numerical.argsort(a, axis=None)\n",
+ "print('\\nflattened a sorted:\\n', c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Since during the sorting, only the indices are shuffled, `argsort` does not modify the input array, as one can verify this by the following example:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-11-06T16:04:31.653444Z",
+ "start_time": "2019-11-06T16:04:31.634995Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "a:\n",
+ " array([0, 5, 1, 3, 2, 4], dtype=uint8)\n",
+ "\n",
+ "sorting indices:\n",
+ " array([0, 2, 4, 3, 5, 1], dtype=uint16)\n",
+ "\n",
+ "the original array:\n",
+ " array([0, 5, 1, 3, 2, 4], dtype=uint8)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import numerical\n",
+ "\n",
+ "a = np.array([0, 5, 1, 3, 2, 4], dtype=np.uint8)\n",
+ "print('\\na:\\n', a)\n",
+ "b = numerical.argsort(a, axis=1)\n",
+ "print('\\nsorting indices:\\n', b)\n",
+ "print('\\nthe original array:\\n', a)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-poly.ipynb b/circuitpython/extmod/ulab/docs/ulab-poly.ipynb
new file mode 100644
index 0000000..9cd7223
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-poly.ipynb
@@ -0,0 +1,454 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-01T09:27:13.438054Z",
+ "start_time": "2020-05-01T09:27:13.191491Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-08-03T18:32:45.342280Z",
+ "start_time": "2020-08-03T18:32:45.338442Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-07-23T20:31:25.296014Z",
+ "start_time": "2020-07-23T20:31:25.265937Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Polynomials\n",
+ "\n",
+ "Functions in the polynomial sub-module can be invoked by importing the module first."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## polyval\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyval.html\n",
+ "\n",
+ "`polyval` takes two arguments, both arrays or other iterables."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 187,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-11-01T12:53:22.448303Z",
+ "start_time": "2019-11-01T12:53:22.435176Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "coefficients: [1, 1, 1, 0]\n",
+ "independent values: [0, 1, 2, 3, 4]\n",
+ "\n",
+ "values of p(x): array([0.0, 3.0, 14.0, 39.0, 84.0], dtype=float)\n",
+ "\n",
+ "ndarray (a): array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)\n",
+ "value of p(a): array([0.0, 3.0, 14.0, 39.0, 84.0], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import poly\n",
+ "\n",
+ "p = [1, 1, 1, 0]\n",
+ "x = [0, 1, 2, 3, 4]\n",
+ "print('coefficients: ', p)\n",
+ "print('independent values: ', x)\n",
+ "print('\\nvalues of p(x): ', poly.polyval(p, x))\n",
+ "\n",
+ "# the same works with one-dimensional ndarrays\n",
+ "a = np.array(x)\n",
+ "print('\\nndarray (a): ', a)\n",
+ "print('value of p(a): ', poly.polyval(p, a))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## polyfit\n",
+ "\n",
+ "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html\n",
+ "\n",
+ "polyfit takes two, or three arguments. The last one is the degree of the polynomial that will be fitted, the last but one is an array or iterable with the `y` (dependent) values, and the first one, an array or iterable with the `x` (independent) values, can be dropped. If that is the case, `x` will be generated in the function, assuming uniform sampling. \n",
+ "\n",
+ "If the length of `x`, and `y` are not the same, the function raises a `ValueError`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 189,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-11-01T12:54:08.326802Z",
+ "start_time": "2019-11-01T12:54:08.311182Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "independent values:\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=float)\n",
+ "dependent values:\t array([9.0, 4.0, 1.0, 0.0, 1.0, 4.0, 9.0], dtype=float)\n",
+ "fitted values:\t\t array([1.0, -6.0, 9.000000000000004], dtype=float)\n",
+ "\n",
+ "dependent values:\t array([9.0, 4.0, 1.0, 0.0, 1.0, 4.0, 9.0], dtype=float)\n",
+ "fitted values:\t\t array([1.0, -6.0, 9.000000000000004], dtype=float)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import poly\n",
+ "\n",
+ "x = np.array([0, 1, 2, 3, 4, 5, 6])\n",
+ "y = np.array([9, 4, 1, 0, 1, 4, 9])\n",
+ "print('independent values:\\t', x)\n",
+ "print('dependent values:\\t', y)\n",
+ "print('fitted values:\\t\\t', poly.polyfit(x, y, 2))\n",
+ "\n",
+ "# the same with missing x\n",
+ "print('\\ndependent values:\\t', y)\n",
+ "print('fitted values:\\t\\t', poly.polyfit(y, 2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Execution time\n",
+ "\n",
+ "`polyfit` is based on the inversion of a matrix (there is more on the background in https://en.wikipedia.org/wiki/Polynomial_regression), and it requires the intermediate storage of `2*N*(deg+1)` floats, where `N` is the number of entries in the input array, and `deg` is the fit's degree. The additional computation costs of the matrix inversion discussed in [inv](#inv) also apply. The example from above needs around 150 microseconds to return:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 560,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2019-10-20T07:24:39.002243Z",
+ "start_time": "2019-10-20T07:24:38.978687Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "execution time: 153 us\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import ulab as np\n",
+ "from ulab import poly\n",
+ "\n",
+ "@timeit\n",
+ "def time_polyfit(x, y, n):\n",
+ " return poly.polyfit(x, y, n)\n",
+ "\n",
+ "x = np.array([0, 1, 2, 3, 4, 5, 6])\n",
+ "y = np.array([9, 4, 1, 0, 1, 4, 9])\n",
+ "\n",
+ "time_polyfit(x, y, 2)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-programming.ipynb b/circuitpython/extmod/ulab/docs/ulab-programming.ipynb
new file mode 100644
index 0000000..6eabf6d
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-programming.ipynb
@@ -0,0 +1,798 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-10-25T21:25:53.804315Z",
+ "start_time": "2020-10-25T21:25:43.765649Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Programming ulab\n",
+ "\n",
+ "Earlier we have seen, how `ulab`'s functions and methods can be accessed in `micropython`. This last section of the book explains, how these functions are implemented. By the end of this chapter, not only would you be able to extend `ulab`, and write your own `numpy`-compatible functions, but through a deeper understanding of the inner workings of the functions, you would also be able to see what the trade-offs are at the `python` level.\n",
+ "\n",
+ "\n",
+ "## Code organisation\n",
+ "\n",
+ "As mentioned earlier, the `python` functions are organised into sub-modules at the C level. The C sub-modules can be found in `./ulab/code/`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The `ndarray` object"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### General comments\n",
+ "\n",
+ "`ndarrays` are efficient containers of numerical data of the same type (i.e., signed/unsigned chars, signed/unsigned integers or `mp_float_t`s, which, depending on the platform, are either C `float`s, or C `double`s). Beyond storing the actual data in the void pointer `*array`, the type definition has eight additional members (on top of the `base` type). Namely, the `dtype`, which tells us, how the bytes are to be interpreted. Moreover, the `itemsize`, which stores the size of a single entry in the array, `boolean`, an unsigned integer, which determines, whether the arrays is to be treated as a set of Booleans, or as numerical data, `ndim`, the number of dimensions (`uint8_t`), `len`, the length of the array (the number of entries), the shape (`*size_t`), the strides (`*int32_t`). The length is simply the product of the numbers in `shape`.\n",
+ "\n",
+ "The type definition is as follows:\n",
+ "\n",
+ "```c\n",
+ "typedef struct _ndarray_obj_t {\n",
+ " mp_obj_base_t base;\n",
+ " uint8_t dtype;\n",
+ " uint8_t itemsize;\n",
+ " uint8_t boolean;\n",
+ " uint8_t ndim;\n",
+ " size_t len;\n",
+ " size_t shape[ULAB_MAX_DIMS];\n",
+ " int32_t strides[ULAB_MAX_DIMS];\n",
+ " void *array;\n",
+ "} ndarray_obj_t;\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Memory layout\n",
+ "\n",
+ "The values of an `ndarray` are stored in a contiguous segment in the RAM. The `ndarray` can be dense, meaning that all numbers in the linear memory segment belong to a linar combination of coordinates, and it can also be sparse, i.e., some elements of the linear storage space will be skipped, when the elements of the tensor are traversed. \n",
+ "\n",
+ "In the RAM, the position of the item $M(n_1, n_2, ..., n_{k-1}, n_k)$ in a dense tensor of rank $k$ is given by the linear combination \n",
+ "\n",
+ "\\begin{equation}\n",
+ "P(n_1, n_2, ..., n_{k-1}, n_k) = n_1 s_1 + n_2 s_2 + ... + n_{k-1}s_{k-1} + n_ks_k = \\sum_{i=1}^{k}n_is_i\n",
+ "\\end{equation}\n",
+ "where $s_i$ are the strides of the tensor, defined as \n",
+ "\n",
+ "\\begin{equation}\n",
+ "s_i = \\prod_{j=i+1}^k l_j\n",
+ "\\end{equation}\n",
+ "\n",
+ "where $l_j$ is length of the tensor along the $j$th axis. When the tensor is sparse (e.g., when the tensor is sliced), the strides along a particular axis will be multiplied by a non-zero integer. If this integer is different to $\\pm 1$, the linear combination above cannot access all elements in the RAM, i.e., some numbers will be skipped. Note that $|s_1| > |s_2| > ... > |s_{k-1}| > |s_k|$, even if the tensor is sparse. The statement is trivial for dense tensors, and it follows from the definition of $s_i$. For sparse tensors, a slice cannot have a step larger than the shape along that axis. But for dense tensors, $s_i/s_{i+1} = l_i$. \n",
+ "\n",
+ "When creating a *view*, we simply re-calculate the `strides`, and re-set the `*array` pointer.\n",
+ "\n",
+ "## Iterating over elements of a tensor\n",
+ "\n",
+ "The `shape` and `strides` members of the array tell us how we have to move our pointer, when we want to read out the numbers. For technical reasons that will become clear later, the numbers in `shape` and in `strides` are aligned to the right, and begin on the right hand side, i.e., if the number of possible dimensions is `ULAB_MAX_DIMS`, then `shape[ULAB_MAX_DIMS-1]` is the length of the last axis, `shape[ULAB_MAX_DIMS-2]` is the length of the last but one axis, and so on. If the number of actual dimensions, `ndim < ULAB_MAX_DIMS`, the first `ULAB_MAX_DIMS - ndim` entries in `shape` and `strides` will be equal to zero, but they could, in fact, be assigned any value, because these will never be accessed in an operation.\n",
+ "\n",
+ "With this definition of the strides, the linear combination in $P(n_1, n_2, ..., n_{k-1}, n_k)$ is a one-to-one mapping from the space of tensor coordinates, $(n_1, n_2, ..., n_{k-1}, n_k)$, and the coordinate in the linear array, $n_1s_1 + n_2s_2 + ... + n_{k-1}s_{k-1} + n_ks_k$, i.e., no two distinct sets of coordinates will result in the same position in the linear array. \n",
+ "\n",
+ "Since the `strides` are given in terms of bytes, when we iterate over an array, the void data pointer is usually cast to `uint8_t`, and the values are converted using the proper data type stored in `ndarray->dtype`. However, there might be cases, when it makes perfect sense to cast `*array` to a different type, in which case the `strides` have to be re-scaled by the value of `ndarray->itemsize`.\n",
+ "\n",
+ "### Iterating using the unwrapped loops\n",
+ "\n",
+ "The following macro definition is taken from [vector.h](https://github.com/v923z/micropython-ulab/blob/master/code/numpy/vector/vector.h), and demonstrates, how we can iterate over a single array in four dimensions. \n",
+ "\n",
+ "```c\n",
+ "#define ITERATE_VECTOR(type, array, source, sarray) do {\n",
+ " size_t i=0;\n",
+ " do {\n",
+ " size_t j = 0;\n",
+ " do {\n",
+ " size_t k = 0;\n",
+ " do {\n",
+ " size_t l = 0;\n",
+ " do {\n",
+ " *(array)++ = f(*((type *)(sarray)));\n",
+ " (sarray) += (source)->strides[ULAB_MAX_DIMS - 1];\n",
+ " l++;\n",
+ " } while(l < (source)->shape[ULAB_MAX_DIMS-1]);\n",
+ " (sarray) -= (source)->strides[ULAB_MAX_DIMS - 1] * (source)->shape[ULAB_MAX_DIMS-1];\n",
+ " (sarray) += (source)->strides[ULAB_MAX_DIMS - 2];\n",
+ " k++;\n",
+ " } while(k < (source)->shape[ULAB_MAX_DIMS-2]);\n",
+ " (sarray) -= (source)->strides[ULAB_MAX_DIMS - 2] * (source)->shape[ULAB_MAX_DIMS-2];\n",
+ " (sarray) += (source)->strides[ULAB_MAX_DIMS - 3];\n",
+ " j++;\n",
+ " } while(j < (source)->shape[ULAB_MAX_DIMS-3]);\n",
+ " (sarray) -= (source)->strides[ULAB_MAX_DIMS - 3] * (source)->shape[ULAB_MAX_DIMS-3];\n",
+ " (sarray) += (source)->strides[ULAB_MAX_DIMS - 4];\n",
+ " i++;\n",
+ " } while(i < (source)->shape[ULAB_MAX_DIMS-4]);\n",
+ "} while(0)\n",
+ "```\n",
+ "\n",
+ "We start with the innermost loop, the one recursing `l`. `array` is already of type `mp_float_t`, while the source array, `sarray`, has been cast to `uint8_t` in the calling function. The numbers contained in `sarray` have to be read out in the proper type dictated by `ndarray->dtype`. This is what happens in the statement `*((type *)(sarray))`, and this number is then fed into the function `f`. Vectorised mathematical functions produce *dense* arrays, and for this reason, we can simply advance the `array` pointer. \n",
+ "\n",
+ "The advancing of the `sarray` pointer is a bit more involving: first, in the innermost loop, we simply move forward by the amount given by the last stride, which is `(source)->strides[ULAB_MAX_DIMS - 1]`, because the `shape` and the `strides` are aligned to the right. We move the pointer as many times as given by `(source)->shape[ULAB_MAX_DIMS-1]`, which is the length of the very last axis. Hence the the structure of the loop\n",
+ "\n",
+ "```c\n",
+ " size_t l = 0;\n",
+ " do {\n",
+ " ...\n",
+ " l++;\n",
+ " } while(l < (source)->shape[ULAB_MAX_DIMS-1]);\n",
+ "\n",
+ "```\n",
+ "Once we have exhausted the last axis, we have to re-wind the pointer, and advance it by an amount given by the last but one stride. Keep in mind that in the the innermost loop we moved our pointer `(source)->shape[ULAB_MAX_DIMS-1]` times by `(source)->strides[ULAB_MAX_DIMS - 1]`, i.e., we re-wind it by moving it backwards by `(source)->strides[ULAB_MAX_DIMS - 1] * (source)->shape[ULAB_MAX_DIMS-1]`. In the next step, we move forward by `(source)->strides[ULAB_MAX_DIMS - 2]`, which is the last but one stride. \n",
+ "\n",
+ "\n",
+ "```c\n",
+ " (sarray) -= (source)->strides[ULAB_MAX_DIMS - 1] * (source)->shape[ULAB_MAX_DIMS-1];\n",
+ " (sarray) += (source)->strides[ULAB_MAX_DIMS - 2];\n",
+ "\n",
+ "```\n",
+ "\n",
+ "This pattern must be repeated for each axis of the array, and this is how we arrive at the four nested loops listed above."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Re-winding arrays by means of a function\n",
+ "\n",
+ "\n",
+ "In addition to un-wrapping the iteration loops by means of macros, there is another way of traversing all elements of a tensor: we note that, since $|s_1| > |s_2| > ... > |s_{k-1}| > |s_k|$, $P(n1, n2, ..., n_{k-1}, n_k)$ changes most slowly in the last coordinate. Hence, if we start from the very beginning, ($n_i = 0$ for all $i$), and walk along the linear RAM segment, we increment the value of $n_k$ as long as $n_k < l_k$. Once $n_k = l_k$, we have to reset $n_k$ to 0, and increment $n_{k-1}$ by one. After each such round, $n_{k-1}$ will be incremented by one, as long as $n_{k-1} < l_{k-1}$. Once $n_{k-1} = l_{k-1}$, we reset both $n_k$, and $n_{k-1}$ to 0, and increment $n_{k-2}$ by one. \n",
+ "\n",
+ "Rewinding the arrays in this way is implemented in the function `ndarray_rewind_array` in [ndarray.c](https://github.com/v923z/micropython-ulab/blob/master/code/ndarray.c). \n",
+ "\n",
+ "```c\n",
+ "void ndarray_rewind_array(uint8_t ndim, uint8_t *array, size_t *shape, int32_t *strides, size_t *coords) {\n",
+ " // resets the data pointer of a single array, whenever an axis is full\n",
+ " // since we always iterate over the very last axis, we have to keep track of\n",
+ " // the last ndim-2 axes only\n",
+ " array -= shape[ULAB_MAX_DIMS - 1] * strides[ULAB_MAX_DIMS - 1];\n",
+ " array += strides[ULAB_MAX_DIMS - 2];\n",
+ " for(uint8_t i=1; i < ndim-1; i++) {\n",
+ " coords[ULAB_MAX_DIMS - 1 - i] += 1;\n",
+ " if(coords[ULAB_MAX_DIMS - 1 - i] == shape[ULAB_MAX_DIMS - 1 - i]) { // we are at a dimension boundary\n",
+ " array -= shape[ULAB_MAX_DIMS - 1 - i] * strides[ULAB_MAX_DIMS - 1 - i];\n",
+ " array += strides[ULAB_MAX_DIMS - 2 - i];\n",
+ " coords[ULAB_MAX_DIMS - 1 - i] = 0;\n",
+ " coords[ULAB_MAX_DIMS - 2 - i] += 1;\n",
+ " } else { // coordinates can change only, if the last coordinate changes\n",
+ " return;\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "```\n",
+ "\n",
+ "and the function would be called as in the snippet below. Note that the innermost loop is factored out, so that we can save the `if(...)` statement for the last axis.\n",
+ "\n",
+ "```c\n",
+ " size_t *coords = ndarray_new_coords(results->ndim);\n",
+ " for(size_t i=0; i < results->len/results->shape[ULAB_MAX_DIMS -1]; i++) {\n",
+ " size_t l = 0;\n",
+ " do {\n",
+ " ...\n",
+ " l++;\n",
+ " } while(l < results->shape[ULAB_MAX_DIMS - 1]);\n",
+ " ndarray_rewind_array(results->ndim, array, results->shape, strides, coords);\n",
+ " } while(0)\n",
+ "\n",
+ "```\n",
+ "\n",
+ "The advantage of this method is that the implementation is independent of the number of dimensions: the iteration requires more or less the same flash space for 2 dimensions as for 22. However, the price we have to pay for this convenience is the extra function call."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Iterating over two ndarrays simultaneously: broadcasting\n",
+ "\n",
+ "Whenever we invoke a binary operator, call a function with two arguments of `ndarray` type, or assign something to an `ndarray`, we have to iterate over two views at the same time. The task is trivial, if the two `ndarray`s in question have the same shape (but not necessarily the same set of strides), because in this case, we can still iterate in the same loop. All that happens is that we move two data pointers in sync.\n",
+ "\n",
+ "The problem becomes a bit more involving, when the shapes of the two `ndarray`s are not identical. For such cases, `numpy` defines so-called broadcasting, which boils down to two rules. \n",
+ "\n",
+ "1. The shapes in the tensor with lower rank has to be prepended with axes of size 1 till the two ranks become equal.\n",
+ "2. Along all axes the two tensors should have the same size, or one of the sizes must be 1. \n",
+ "\n",
+ "If, after applying the first rule the second is not satisfied, the two `ndarray`s cannot be broadcast together. \n",
+ "\n",
+ "Now, let us suppose that we have two compatible `ndarray`s, i.e., after applying the first rule, the second is satisfied. How do we iterate over the elements in the tensors? \n",
+ "\n",
+ "We should recall, what exactly we do, when iterating over a single array: normally, we move the data pointer by the last stride, except, when we arrive at a dimension boundary (when the last axis is exhausted). At that point, we move the pointer by an amount dictated by the strides. And this is the key: *dictated by the strides*. Now, if we have two arrays that are originally not compatible, we define new strides for them, and use these in the iteration. With that, we are back to the case, where we had two compatible arrays. \n",
+ "\n",
+ "Now, let us look at the second broadcasting rule: if the two arrays have the same size, we take both `ndarray`s' strides along that axis. If, on the other hand, one of the `ndarray`s is of length 1 along one of its axes, we set the corresponding strides to 0. This will ensure that that data pointer is not moved, when we iterate over both `ndarray`s at the same time. \n",
+ "\n",
+ "Thus, in order to implement broadcasting, we first have to check, whether the two above-mentioned rules can be satisfied, and if so, we have to find the two new sets strides. \n",
+ "\n",
+ "The `ndarray_can_broadcast` function from [ndarray.c](https://github.com/v923z/micropython-ulab/blob/master/code/ndarray.c) takes two `ndarray`s, and returns `true`, if the two arrays can be broadcast together. At the same time, it also calculates new strides for the two arrays, so that they can be iterated over at the same time. \n",
+ "\n",
+ "```c\n",
+ "bool ndarray_can_broadcast(ndarray_obj_t *lhs, ndarray_obj_t *rhs, uint8_t *ndim, size_t *shape, int32_t *lstrides, int32_t *rstrides) {\n",
+ " // returns True or False, depending on, whether the two arrays can be broadcast together\n",
+ " // numpy's broadcasting rules are as follows:\n",
+ " //\n",
+ " // 1. the two shapes are either equal\n",
+ " // 2. one of the shapes is 1\n",
+ " memset(lstrides, 0, sizeof(size_t)*ULAB_MAX_DIMS);\n",
+ " memset(rstrides, 0, sizeof(size_t)*ULAB_MAX_DIMS);\n",
+ " lstrides[ULAB_MAX_DIMS - 1] = lhs->strides[ULAB_MAX_DIMS - 1];\n",
+ " rstrides[ULAB_MAX_DIMS - 1] = rhs->strides[ULAB_MAX_DIMS - 1];\n",
+ " for(uint8_t i=ULAB_MAX_DIMS; i > 0; i--) {\n",
+ " if((lhs->shape[i-1] == rhs->shape[i-1]) || (lhs->shape[i-1] == 0) || (lhs->shape[i-1] == 1) ||\n",
+ " (rhs->shape[i-1] == 0) || (rhs->shape[i-1] == 1)) {\n",
+ " shape[i-1] = MAX(lhs->shape[i-1], rhs->shape[i-1]);\n",
+ " if(shape[i-1] > 0) (*ndim)++;\n",
+ " if(lhs->shape[i-1] < 2) {\n",
+ " lstrides[i-1] = 0;\n",
+ " } else {\n",
+ " lstrides[i-1] = lhs->strides[i-1];\n",
+ " }\n",
+ " if(rhs->shape[i-1] < 2) {\n",
+ " rstrides[i-1] = 0;\n",
+ " } else {\n",
+ " rstrides[i-1] = rhs->strides[i-1];\n",
+ " }\n",
+ " } else {\n",
+ " return false;\n",
+ " }\n",
+ " }\n",
+ " return true;\n",
+ "}\n",
+ "```\n",
+ "\n",
+ "A good example of how the function would be called can be found in [vector.c](https://github.com/v923z/micropython-ulab/blob/master/code/numpy/vector/vector.c), in the `vector_arctan2` function:\n",
+ "\n",
+ "```c\n",
+ "mp_obj_t vector_arctan2(mp_obj_t y, mp_obj_t x) {\n",
+ " ...\n",
+ " uint8_t ndim = 0;\n",
+ " size_t *shape = m_new(size_t, ULAB_MAX_DIMS);\n",
+ " int32_t *xstrides = m_new(int32_t, ULAB_MAX_DIMS);\n",
+ " int32_t *ystrides = m_new(int32_t, ULAB_MAX_DIMS);\n",
+ " if(!ndarray_can_broadcast(ndarray_x, ndarray_y, &ndim, shape, xstrides, ystrides)) {\n",
+ " mp_raise_ValueError(translate(\"operands could not be broadcast together\"));\n",
+ " m_del(size_t, shape, ULAB_MAX_DIMS);\n",
+ " m_del(int32_t, xstrides, ULAB_MAX_DIMS);\n",
+ " m_del(int32_t, ystrides, ULAB_MAX_DIMS);\n",
+ " }\n",
+ "\n",
+ " uint8_t *xarray = (uint8_t *)ndarray_x->array;\n",
+ " uint8_t *yarray = (uint8_t *)ndarray_y->array;\n",
+ " \n",
+ " ndarray_obj_t *results = ndarray_new_dense_ndarray(ndim, shape, NDARRAY_FLOAT);\n",
+ " mp_float_t *rarray = (mp_float_t *)results->array;\n",
+ " ...\n",
+ "```\n",
+ "\n",
+ "After the new strides have been calculated, the iteration loop is identical to what we discussed in the previous section."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Contracting an `ndarray`\n",
+ "\n",
+ "\n",
+ "There are many operations that reduce the number of dimensions of an `ndarray` by 1, i.e., that remove an axis from the tensor. The drill is the same as before, with the exception that first we have to remove the `strides` and `shape` that corresponds to the axis along which we intend to contract. The `numerical_reduce_axes` function from [numerical.c](https://github.com/v923z/micropython-ulab/blob/master/code/numerical/numerical.c) does that. \n",
+ "\n",
+ "\n",
+ "```c\n",
+ "static void numerical_reduce_axes(ndarray_obj_t *ndarray, int8_t axis, size_t *shape, int32_t *strides) {\n",
+ " // removes the values corresponding to a single axis from the shape and strides array\n",
+ " uint8_t index = ULAB_MAX_DIMS - ndarray->ndim + axis;\n",
+ " if((ndarray->ndim == 1) && (axis == 0)) {\n",
+ " index = 0;\n",
+ " shape[ULAB_MAX_DIMS - 1] = 0;\n",
+ " return;\n",
+ " }\n",
+ " for(uint8_t i = ULAB_MAX_DIMS - 1; i > 0; i--) {\n",
+ " if(i > index) {\n",
+ " shape[i] = ndarray->shape[i];\n",
+ " strides[i] = ndarray->strides[i];\n",
+ " } else {\n",
+ " shape[i] = ndarray->shape[i-1];\n",
+ " strides[i] = ndarray->strides[i-1];\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "```\n",
+ "\n",
+ "Once the reduced `strides` and `shape` are known, we place the axis in question in the innermost loop, and wrap it with the loops, whose coordinates are in the `strides`, and `shape` arrays. The `RUN_STD` macro from [numerical.h](https://github.com/v923z/micropython-ulab/blob/master/code/numpy/numerical/numerical.h) is a good example. The macro is expanded in the `numerical_sum_mean_std_ndarray` function. \n",
+ "\n",
+ "\n",
+ "```c\n",
+ "static mp_obj_t numerical_sum_mean_std_ndarray(ndarray_obj_t *ndarray, mp_obj_t axis, uint8_t optype, size_t ddof) {\n",
+ " uint8_t *array = (uint8_t *)ndarray->array;\n",
+ " size_t *shape = m_new(size_t, ULAB_MAX_DIMS);\n",
+ " memset(shape, 0, sizeof(size_t)*ULAB_MAX_DIMS);\n",
+ " int32_t *strides = m_new(int32_t, ULAB_MAX_DIMS);\n",
+ " memset(strides, 0, sizeof(uint32_t)*ULAB_MAX_DIMS);\n",
+ "\n",
+ " int8_t ax = mp_obj_get_int(axis);\n",
+ " if(ax < 0) ax += ndarray->ndim;\n",
+ " if((ax < 0) || (ax > ndarray->ndim - 1)) {\n",
+ " mp_raise_ValueError(translate(\"index out of range\"));\n",
+ " }\n",
+ " numerical_reduce_axes(ndarray, ax, shape, strides);\n",
+ " uint8_t index = ULAB_MAX_DIMS - ndarray->ndim + ax;\n",
+ " ndarray_obj_t *results = NULL;\n",
+ " uint8_t *rarray = NULL;\n",
+ " ...\n",
+ "\n",
+ "```\n",
+ "Here is the macro for the three-dimensional case: \n",
+ "\n",
+ "```c\n",
+ "#define RUN_STD(ndarray, type, array, results, r, shape, strides, index, div) do {\n",
+ " size_t k = 0;\n",
+ " do {\n",
+ " size_t l = 0;\n",
+ " do {\n",
+ " RUN_STD1((ndarray), type, (array), (results), (r), (index), (div));\n",
+ " (array) -= (ndarray)->strides[(index)] * (ndarray)->shape[(index)];\n",
+ " (array) += (strides)[ULAB_MAX_DIMS - 1];\n",
+ " l++;\n",
+ " } while(l < (shape)[ULAB_MAX_DIMS - 1]);\n",
+ " (array) -= (strides)[ULAB_MAX_DIMS - 2] * (shape)[ULAB_MAX_DIMS-2];\n",
+ " (array) += (strides)[ULAB_MAX_DIMS - 3];\n",
+ " k++;\n",
+ " } while(k < (shape)[ULAB_MAX_DIMS - 2]);\n",
+ "} while(0)\n",
+ "```\n",
+ "In `RUN_STD`, we simply move our pointers; the calculation itself happens in the `RUN_STD1` macro below. (Note that this is the implementation of the numerically stable Welford algorithm.)\n",
+ "\n",
+ "```c\n",
+ "#define RUN_STD1(ndarray, type, array, results, r, index, div)\n",
+ "({\n",
+ " mp_float_t M, m, S = 0.0, s = 0.0;\n",
+ " M = m = *(mp_float_t *)((type *)(array));\n",
+ " for(size_t i=1; i < (ndarray)->shape[(index)]; i++) {\n",
+ " (array) += (ndarray)->strides[(index)];\n",
+ " mp_float_t value = *(mp_float_t *)((type *)(array));\n",
+ " m = M + (value - M) / (mp_float_t)i;\n",
+ " s = S + (value - M) * (value - m);\n",
+ " M = m;\n",
+ " S = s;\n",
+ " }\n",
+ " (array) += (ndarray)->strides[(index)];\n",
+ " *(r)++ = MICROPY_FLOAT_C_FUN(sqrt)((ndarray)->shape[(index)] * s / (div));\n",
+ "})\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Upcasting\n",
+ "\n",
+ "When in an operation the `dtype`s of two arrays are different, the result's `dtype` will be decided by the following upcasting rules:\n",
+ "\n",
+ "1. Operations with two `ndarray`s of the same `dtype` preserve their `dtype`, even when the results overflow.\n",
+ "\n",
+ "2. if either of the operands is a float, the result automatically becomes a float\n",
+ "\n",
+ "3. otherwise\n",
+ "\n",
+ " - `uint8` + `int8` => `int16`, \n",
+ " - `uint8` + `int16` => `int16`\n",
+ " - `uint8` + `uint16` => `uint16`\n",
+ " \n",
+ " - `int8` + `int16` => `int16`\n",
+ " - `int8` + `uint16` => `uint16` (in numpy, the result is a `int32`)\n",
+ "\n",
+ " - `uint16` + `int16` => `float` (in numpy, the result is a `int32`)\n",
+ " \n",
+ "4. When one operand of a binary operation is a generic scalar `micropython` variable, i.e., `mp_obj_int`, or `mp_obj_float`, it will be converted to a linear array of length 1, and with the smallest `dtype` that can accommodate the variable in question. After that the broadcasting rules apply, as described in the section [Iterating over two ndarrays simultaneously: broadcasting](#Iterating_over_two_ndarrays_simultaneously:_broadcasting)\n",
+ "\n",
+ "Upcasting is resolved in place, wherever it is required. Notable examples can be found in [ndarray_operators.c](https://github.com/v923z/micropython-ulab/blob/master/code/ndarray_operators.c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Slicing and indexing"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "An `ndarray` can be indexed with three types of objects: integer scalars, slices, and another `ndarray`, whose elements are either integer scalars, or Booleans. Since slice and integer indices can be thought of as modifications of the `strides`, these indices return a view of the `ndarray`. This statement does not hold for `ndarray` indices, and therefore, the return a copy of the array."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Extending ulab\n",
+ "\n",
+ "The `user` module is disabled by default, as can be seen from the last couple of lines of [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h)\n",
+ "\n",
+ "```c\n",
+ "// user-defined module\n",
+ "#ifndef ULAB_USER_MODULE\n",
+ "#define ULAB_USER_MODULE (0)\n",
+ "#endif\n",
+ "```\n",
+ "\n",
+ "The module contains a very simple function, `user_dummy`, and this function is bound to the module itself. In other words, even if the module is enabled, one has to `import`:\n",
+ "\n",
+ "```python\n",
+ "\n",
+ "import ulab\n",
+ "from ulab import user\n",
+ "\n",
+ "user.dummy_function(2.5)\n",
+ "```\n",
+ "which should just return 5.0. Even if `numpy`-compatibility is required (i.e., if most functions are bound at the top level to `ulab` directly), having to `import` the module has a great advantage. Namely, only the [user.h](https://github.com/v923z/micropython-ulab/blob/master/code/user/user.h) and [user.c](https://github.com/v923z/micropython-ulab/blob/master/code/user/user.c) files have to be modified, thus it should be relatively straightforward to update your local copy from [github](https://github.com/v923z/micropython-ulab/blob/master/). \n",
+ "\n",
+ "Now, let us see, how we can add a more meaningful function. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Creating a new ndarray\n",
+ "\n",
+ "In the [General comments](#General_comments) sections we have seen the type definition of an `ndarray`. This structure can be generated by means of a couple of functions listed in [ndarray.c](https://github.com/v923z/micropython-ulab/blob/master/code/ndarray.c). \n",
+ "\n",
+ "\n",
+ "### ndarray_new_ndarray\n",
+ "\n",
+ "The `ndarray_new_ndarray` functions is called by all other array-generating functions. It takes the number of dimensions, `ndim`, a `uint8_t`, the `shape`, a pointer to `size_t`, the `strides`, a pointer to `int32_t`, and `dtype`, another `uint8_t` as its arguments, and returns a new array with all entries initialised to 0. \n",
+ "\n",
+ "Assuming that `ULAB_MAX_DIMS > 2`, a new dense array of dimension 3, of `shape` (3, 4, 5), of `strides` (1000, 200, 10), and `dtype` `uint16_t` can be generated by the following instructions\n",
+ "\n",
+ "```c\n",
+ "size_t *shape = m_new(size_t, ULAB_MAX_DIMS);\n",
+ "shape[ULAB_MAX_DIMS - 1] = 5;\n",
+ "shape[ULAB_MAX_DIMS - 2] = 4;\n",
+ "shape[ULAB_MAX_DIMS - 3] = 3;\n",
+ "\n",
+ "int32_t *strides = m_new(int32_t, ULAB_MAX_DIMS);\n",
+ "strides[ULAB_MAX_DIMS - 1] = 10;\n",
+ "strides[ULAB_MAX_DIMS - 2] = 200;\n",
+ "strides[ULAB_MAX_DIMS - 3] = 1000;\n",
+ "\n",
+ "ndarray_obj_t *new_ndarray = ndarray_new_ndarray(3, shape, strides, NDARRAY_UINT16);\n",
+ "```\n",
+ "\n",
+ "### ndarray_new_dense_ndarray\n",
+ "\n",
+ "The functions simply calculates the `strides` from the `shape`, and calls `ndarray_new_ndarray`. Assuming that `ULAB_MAX_DIMS > 2`, a new dense array of dimension 3, of `shape` (3, 4, 5), and `dtype` `mp_float_t` can be generated by the following instructions\n",
+ "\n",
+ "```c\n",
+ "size_t *shape = m_new(size_t, ULAB_MAX_DIMS);\n",
+ "shape[ULAB_MAX_DIMS - 1] = 5;\n",
+ "shape[ULAB_MAX_DIMS - 2] = 4;\n",
+ "shape[ULAB_MAX_DIMS - 3] = 3;\n",
+ "\n",
+ "ndarray_obj_t *new_ndarray = ndarray_new_dense_ndarray(3, shape, NDARRAY_FLOAT);\n",
+ "```\n",
+ "\n",
+ "### ndarray_new_linear_array\n",
+ "\n",
+ "Since the dimensions of a linear array are known (1), the `ndarray_new_linear_array` takes the `length`, a `size_t`, and the `dtype`, an `uint8_t`. Internally, `ndarray_new_linear_array` generates the `shape` array, and calls `ndarray_new_dense_array` with `ndim = 1`.\n",
+ "\n",
+ "A linear array of length 100, and `dtype` `uint8` could be created by the function call\n",
+ "\n",
+ "```c\n",
+ "ndarray_obj_t *new_ndarray = ndarray_new_linear_array(100, NDARRAY_UINT8)\n",
+ "```\n",
+ "\n",
+ "### ndarray_new_ndarray_from_tuple\n",
+ "\n",
+ "This function takes a `tuple`, which should hold the lengths of the axes (in other words, the `shape`), and the `dtype`, and calls internally `ndarray_new_dense_array`. A new `ndarray` can be generated by calling \n",
+ "\n",
+ "```c\n",
+ "ndarray_obj_t *new_ndarray = ndarray_new_ndarray_from_tuple(shape, NDARRAY_FLOAT);\n",
+ "```\n",
+ "where `shape` is a tuple.\n",
+ "\n",
+ "\n",
+ "### ndarray_new_view\n",
+ "\n",
+ "This function crates a *view*, and takes the source, an `ndarray`, the number of dimensions, an `uint8_t`, the `shape`, a pointer to `size_t`, the `strides`, a pointer to `int32_t`, and the offset, an `int32_t` as arguments. The offset is the number of bytes by which the void `array` pointer is shifted. E.g., the `python` statement\n",
+ "\n",
+ "```python\n",
+ "a = np.array([0, 1, 2, 3, 4, 5], dtype=uint8)\n",
+ "b = a[1::2]\n",
+ "```\n",
+ "\n",
+ "produces the array\n",
+ "\n",
+ "```python\n",
+ "array([1, 3, 5], dtype=uint8)\n",
+ "```\n",
+ "which holds its data at position `x0 + 1`, if `a`'s pointer is at `x0`. In this particular case, the offset is 1. \n",
+ "\n",
+ "The array `b` from the example above could be generated as \n",
+ "\n",
+ "```c\n",
+ "size_t *shape = m_new(size_t, ULAB_MAX_DIMS);\n",
+ "shape[ULAB_MAX_DIMS - 1] = 3;\n",
+ "\n",
+ "int32_t *strides = m_new(int32_t, ULAB_MAX_DIMS);\n",
+ "strides[ULAB_MAX_DIMS - 1] = 2;\n",
+ "\n",
+ "int32_t offset = 1;\n",
+ "uint8_t ndim = 1;\n",
+ "\n",
+ "ndarray_obj_t *new_ndarray = ndarray_new_view(ndarray_a, ndim, shape, strides, offset);\n",
+ "```\n",
+ "\n",
+ "### ndarray_copy_array\n",
+ "\n",
+ "The `ndarray_copy_array` function can be used for copying the contents of an array. Note that the target array has to be created beforehand. E.g., a one-to-one copy can be gotten by \n",
+ "\n",
+ "```c\n",
+ "ndarray_obj_t *new_ndarray = ndarray_new_ndarray(source->ndim, source->shape, source->strides, source->dtype);\n",
+ "ndarray_copy_array(source, new_ndarray);\n",
+ "\n",
+ "```\n",
+ "Note that the function cannot be used for forcing type conversion, i.e., the input and output types must be identical, because the function simply calls the `memcpy` function. On the other hand, the input and output `strides` do not necessarily have to be equal.\n",
+ "\n",
+ "### ndarray_copy_view\n",
+ "\n",
+ "The `ndarray_obj_t *new_ndarray = ...` instruction can be saved by calling the `ndarray_copy_view` function with the single `source` argument. \n",
+ "\n",
+ "\n",
+ "## Accessing data in the ndarray\n",
+ "\n",
+ "Having seen, how arrays can be generated and copied, it is time to look at how the data in an `ndarray` can be accessed and modified. \n",
+ "\n",
+ "For starters, let us suppose that the object in question comes from the user (i.e., via the `micropython` interface), First, we have to acquire a pointer to the `ndarray` by calling \n",
+ "\n",
+ "```c\n",
+ "ndarray_obj_t *ndarray = MP_OBJ_TO_PTR(object_in);\n",
+ "```\n",
+ "\n",
+ "If it is not clear, whether the object is an `ndarray` (e.g., if we want to write a function that can take `ndarray`s, and other iterables as its argument), we find this out by evaluating \n",
+ "\n",
+ "```c\n",
+ "mp_obj_is_type(object_in, &ulab_ndarray_type)\n",
+ "```\n",
+ "which should return `true`. Once the pointer is at our disposal, we can get a pointer to the underlying numerical array as discussed earlier, i.e., \n",
+ "\n",
+ "```c\n",
+ "uint8_t *array = (uint8_t *)ndarray->array;\n",
+ "```\n",
+ "\n",
+ "If you need to find out the `dtype` of the array, you can get it by accessing the `dtype` member of the `ndarray`, i.e., \n",
+ "\n",
+ "```c\n",
+ "ndarray->dtype\n",
+ "```\n",
+ "should be equal to `B`, `b`, `H`, `h`, or `f`. The size of a single item is stored in the `itemsize` member. This number should be equal to 1, if the `dtype` is `B`, or `b`, 2, if the `dtype` is `H`, or `h`, 4, if the `dtype` is `f`, and 8 for `d`. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Boilerplate\n",
+ "\n",
+ "In the next section, we will construct a function that generates the element-wise square of a dense array, otherwise, raises a `TypeError` exception. Dense arrays can easily be iterated over, since we do not have to care about the `shape` and the `strides`. If the array is sparse, the section [Iterating over elements of a tensor](#Iterating-over-elements-of-a-tensor) should contain hints as to how the iteration can be implemented.\n",
+ "\n",
+ "The function is listed under [user.c](https://github.com/v923z/micropython-ulab/tree/master/code/user/). The `user` module is bound to `ulab` in [ulab.c](https://github.com/v923z/micropython-ulab/tree/master/code/ulab.c) in the lines \n",
+ "\n",
+ "```c\n",
+ " #if ULAB_USER_MODULE\n",
+ " { MP_ROM_QSTR(MP_QSTR_user), MP_ROM_PTR(&ulab_user_module) },\n",
+ " #endif\n",
+ "```\n",
+ "which assumes that at the very end of [ulab.h](https://github.com/v923z/micropython-ulab/tree/master/code/ulab.h) the \n",
+ "\n",
+ "```c\n",
+ "// user-defined module\n",
+ "#ifndef ULAB_USER_MODULE\n",
+ "#define ULAB_USER_MODULE (1)\n",
+ "#endif\n",
+ "```\n",
+ "constant has been set to 1. After compilation, you can call a particular `user` function in `python` by importing the module first, i.e., \n",
+ "\n",
+ "```python\n",
+ "from ulab import numpy as np\n",
+ "from ulab import user\n",
+ "\n",
+ "user.some_function(...)\n",
+ "```\n",
+ "\n",
+ "This separation of user-defined functions from the rest of the code ensures that the integrity of the main module and all its functions are always preserved. Even in case of a catastrophic failure, you can exclude the `user` module, and start over.\n",
+ "\n",
+ "And now the function:\n",
+ "\n",
+ "\n",
+ "```c\n",
+ "static mp_obj_t user_square(mp_obj_t arg) {\n",
+ " // the function takes a single dense ndarray, and calculates the \n",
+ " // element-wise square of its entries\n",
+ " \n",
+ " // raise a TypeError exception, if the input is not an ndarray\n",
+ " if(!mp_obj_is_type(arg, &ulab_ndarray_type)) {\n",
+ " mp_raise_TypeError(translate(\"input must be an ndarray\"));\n",
+ " }\n",
+ " ndarray_obj_t *ndarray = MP_OBJ_TO_PTR(arg);\n",
+ " \n",
+ " // make sure that the input is a dense array\n",
+ " if(!ndarray_is_dense(ndarray)) {\n",
+ " mp_raise_TypeError(translate(\"input must be a dense ndarray\"));\n",
+ " }\n",
+ " \n",
+ " // if the input is a dense array, create `results` with the same number of \n",
+ " // dimensions, shape, and dtype\n",
+ " ndarray_obj_t *results = ndarray_new_dense_ndarray(ndarray->ndim, ndarray->shape, ndarray->dtype);\n",
+ " \n",
+ " // since in a dense array the iteration over the elements is trivial, we \n",
+ " // can cast the data arrays ndarray->array and results->array to the actual type\n",
+ " if(ndarray->dtype == NDARRAY_UINT8) {\n",
+ " uint8_t *array = (uint8_t *)ndarray->array;\n",
+ " uint8_t *rarray = (uint8_t *)results->array;\n",
+ " for(size_t i=0; i < ndarray->len; i++, array++) {\n",
+ " *rarray++ = (*array) * (*array);\n",
+ " }\n",
+ " } else if(ndarray->dtype == NDARRAY_INT8) {\n",
+ " int8_t *array = (int8_t *)ndarray->array;\n",
+ " int8_t *rarray = (int8_t *)results->array;\n",
+ " for(size_t i=0; i < ndarray->len; i++, array++) {\n",
+ " *rarray++ = (*array) * (*array);\n",
+ " }\n",
+ " } else if(ndarray->dtype == NDARRAY_UINT16) {\n",
+ " uint16_t *array = (uint16_t *)ndarray->array;\n",
+ " uint16_t *rarray = (uint16_t *)results->array;\n",
+ " for(size_t i=0; i < ndarray->len; i++, array++) {\n",
+ " *rarray++ = (*array) * (*array);\n",
+ " }\n",
+ " } else if(ndarray->dtype == NDARRAY_INT16) {\n",
+ " int16_t *array = (int16_t *)ndarray->array;\n",
+ " int16_t *rarray = (int16_t *)results->array;\n",
+ " for(size_t i=0; i < ndarray->len; i++, array++) {\n",
+ " *rarray++ = (*array) * (*array);\n",
+ " }\n",
+ " } else { // if we end up here, the dtype is NDARRAY_FLOAT\n",
+ " mp_float_t *array = (mp_float_t *)ndarray->array;\n",
+ " mp_float_t *rarray = (mp_float_t *)results->array;\n",
+ " for(size_t i=0; i < ndarray->len; i++, array++) {\n",
+ " *rarray++ = (*array) * (*array);\n",
+ " } \n",
+ " }\n",
+ " // at the end, return a micropython object\n",
+ " return MP_OBJ_FROM_PTR(results);\n",
+ "}\n",
+ "\n",
+ "```\n",
+ "\n",
+ "To summarise, the steps for *implementing* a function are\n",
+ "\n",
+ "1. If necessary, inspect the type of the input object, which is always a `mp_obj_t` object\n",
+ "2. If the input is an `ndarray_obj_t`, acquire a pointer to it by calling `ndarray_obj_t *ndarray = MP_OBJ_TO_PTR(arg);`\n",
+ "3. Create a new array, or modify the existing one; get a pointer to the data by calling `uint8_t *array = (uint8_t *)ndarray->array;`, or something equivalent\n",
+ "4. Once the new data have been calculated, return a `micropython` object by calling `MP_OBJ_FROM_PTR(...)`.\n",
+ "\n",
+ "The listing above contains the implementation of the function, but as such, it cannot be called from `python`: \n",
+ "it still has to be bound to the name space. This we do by first defining a function object in \n",
+ "\n",
+ "```c\n",
+ "MP_DEFINE_CONST_FUN_OBJ_1(user_square_obj, user_square);\n",
+ "\n",
+ "```\n",
+ "\n",
+ "`micropython` defines a number of `MP_DEFINE_CONST_FUN_OBJ_N` macros in [obj.h](https://github.com/micropython/micropython/blob/master/py/obj.h). `N` is always the number of arguments the function takes. We had a function definition `static mp_obj_t user_square(mp_obj_t arg)`, i.e., we dealt with a single argument. \n",
+ "\n",
+ "Finally, we have to bind this function object in the globals table of the `user` module: \n",
+ "\n",
+ "```c\n",
+ "STATIC const mp_rom_map_elem_t ulab_user_globals_table[] = {\n",
+ " { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_user) },\n",
+ " { MP_OBJ_NEW_QSTR(MP_QSTR_square), (mp_obj_t)&user_square_obj },\n",
+ "};\n",
+ "```\n",
+ "\n",
+ "Thus, the three steps required for the definition of a user-defined function are \n",
+ "\n",
+ "1. The low-level implementation of the function itself\n",
+ "2. The definition of a function object by calling MP_DEFINE_CONST_FUN_OBJ_N()\n",
+ "3. Binding this function object to the namespace in the `ulab_user_globals_table[]`"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {},
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/circuitpython/extmod/ulab/docs/ulab-tricks.ipynb b/circuitpython/extmod/ulab/docs/ulab-tricks.ipynb
new file mode 100644
index 0000000..ec67c8c
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-tricks.ipynb
@@ -0,0 +1,582 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-01T09:27:13.438054Z",
+ "start_time": "2020-05-01T09:27:13.191491Z"
+ }
+ },
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-08-03T18:32:45.342280Z",
+ "start_time": "2020-08-03T18:32:45.338442Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-07-23T20:31:25.296014Z",
+ "start_time": "2020-07-23T20:31:25.265937Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Tricks\n",
+ "\n",
+ "This section of the book discusses a couple of tricks that can be exploited to either speed up computations, or save on RAM. However, there is probably no silver bullet, and you have to evaluate your code in terms of execution speed (if the execution is time critical), or RAM used. You should also keep in mind that, if a particular code snippet is optimised on some hardware, there is no guarantee that on another piece of hardware, you will get similar improvements. Hardware implementations are vastly different. Some microcontrollers do not even have an FPU, so you should not be surprised that you get significantly different benchmarks. Just to underline this statement, you can study the [collection of benchmarks](https://github.com/thiagofe/ulab_samples)."
+ ]
+ },
+ {
+ "source": [
+ "## Use an `ndarray`, if you can\n",
+ "\n",
+ "Many functions in `ulab` are implemented in a universal fashion, meaning that both generic `micropython` iterables, and `ndarray`s can be passed as an argument. E.g., both \n",
+ "\n",
+ "```python\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "np.sum([1, 2, 3, 4, 5])\n",
+ "```\n",
+ "and\n",
+ "\n",
+ "```python\n",
+ "from ulab import numpy as np\n",
+ "\n",
+ "a = np.array([1, 2, 3, 4, 5])\n",
+ "np.sum(a)\n",
+ "```\n",
+ "\n",
+ "will return the `micropython` variable 15 as the result. Still, `np.sum(a)` is evaluated significantly faster, because in `np.sum([1, 2, 3, 4, 5])`, the interpreter has to fetch 5 `micropython` variables, convert them to `float`, and sum the values, while the C type of `a` is known, thus the interpreter can invoke a single `for` loop for the evaluation of the `sum`. In the `for` loop, there are no function calls, the iteration simply walks through the pointer holding the values of `a`, and adds the values to an accumulator. If the array `a` is already available, then you can gain a factor of 3 in speed by calling `sum` on the array, instead of using the list. Compared to the python implementation of the same functionality, the speed-up is around 40 (again, this might depend on the hardware).\n",
+ "\n",
+ "On the other hand, if the array is not available, then there is not much point in converting the list to an `ndarray` and passing that to the function. In fact, you should expect a slow-down: the constructor has to iterate over the list elements, and has to convert them to a numerical type. On top of that, it also has to reserve RAM for the `ndarray`."
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "## Use a reasonable `dtype`\n",
+ "\n",
+ "Just as in `numpy`, the default `dtype` is `float`. But this does not mean that that is the most suitable one in all scenarios. If data are streamed from an 8-bit ADC, and you only want to know the maximum, or the sum, then it is quite reasonable to use `uint8` for the `dtype`. Storing the same data in `float` array would cost 4 or 8 times as much RAM, with absolutely no gain. Do not rely on the default value of the constructor's keyword argument, and choose one that fits!"
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "## Beware the axis!\n",
+ "\n",
+ "Whenever `ulab` iterates over multi-dimensional arrays, the outermost loop is the first axis, then the second axis, and so on. E.g., when the `sum` of \n",
+ "\n",
+ "```python\n",
+ "a = array([[1, 2, 3, 4],\n",
+ " [5, 6, 7, 8], \n",
+ " [9, 10, 11, 12]], dtype=uint8)\n",
+ "```\n",
+ "\n",
+ "is being calculated, first the data pointer walks along `[1, 2, 3, 4]` (innermost loop, last axis), then is moved back to the position, where 5 is stored (this is the nesting loop), and traverses `[5, 6, 7, 8]`, and so on. Moving the pointer back to 5 is more expensive, than moving it along an axis, because the position of 5 has to be calculated, whereas moving from 5 to 6 is simply an addition to the address. Thus, while the matrix\n",
+ "\n",
+ "```python\n",
+ "b = array([[1, 5, 9],\n",
+ " [2, 6, 10], \n",
+ " [3, 7, 11],\n",
+ " [4, 8, 12]], dtype=uint8)\n",
+ "```\n",
+ "\n",
+ "holds the same data as `a`, the summation over the entries in `b` is slower, because the pointer has to be re-wound three times, as opposed to twice in `a`. For small matrices the savings are not significant, but you would definitely notice the difference, if you had \n",
+ "\n",
+ "```\n",
+ "a = array(range(2000)).reshape((2, 1000))\n",
+ "b = array(range(2000)).reshape((1000, 2))\n",
+ "```\n",
+ "\n",
+ "The moral is that, in order to improve on the execution speed, whenever possible, you should try to make the last axis the longest. As a side note, `numpy` can re-arrange its loops, and puts the longest axis in the innermost loop. This is why the longest axis is sometimes referred to as the fast axis. In `ulab`, the order of the axes is fixed. "
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "## Reduce the number of artifacts\n",
+ "\n",
+ "Before showing a real-life example, let us suppose that we want to interpolate uniformly sampled data, and the absolute magnitude is not really important, we only care about the ratios between neighbouring value. One way of achieving this is calling the `interp` functions. However, we could just as well work with slices."
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "array([ 0, 5, 10, 6, 2, 11, 20, 12, 4], dtype=uint8)"
+ ]
+ },
+ "metadata": {},
+ "execution_count": 18
+ }
+ ],
+ "source": [
+ "a = array([0, 10, 2, 20, 4], dtype=np.uint8)\n",
+ "b = np.zeros(9, dtype=np.uint8)\n",
+ "\n",
+ "b[::2] = 2 * a\n",
+ "b[1::2] = a[:-1] + a[1:]\n",
+ "\n",
+ "b //= 2\n",
+ "b"
+ ]
+ },
+ {
+ "source": [
+ "`b` now has values from `a` at every even position, and interpolates the values on every odd position. If only the relative magnitudes are important, then we can even save the division by 2, and we end up with "
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "a = array([0, 10, 2, 20, 4], dtype=np.uint8)\n",
+ "b = np.zeros(9, dtype=np.uint8)\n",
+ "\n",
+ "b[::2] = 2 * a\n",
+ "b[1::2] = a[:-1] + a[1:]\n",
+ "\n",
+ "b"
+ ],
+ "cell_type": "code",
+ "metadata": {},
+ "execution_count": 16,
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "array([ 0, 10, 20, 12, 4, 22, 40, 24, 8], dtype=uint8)"
+ ]
+ },
+ "metadata": {},
+ "execution_count": 16
+ }
+ ]
+ },
+ {
+ "source": [
+ "Importantly, we managed to keep the results in the smaller `dtype`, `uint8`. Now, while the two assignments above are terse and pythonic, the code is not the most efficient: the right hand sides are compound statements, generating intermediate results. To store them, RAM has to be allocated. This takes time, and leads to memory fragmentation. Better is to write out the assignments in 4 instructions:"
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "array([ 0, 10, 20, 12, 4, 22, 40, 24, 8], dtype=uint8)"
+ ]
+ },
+ "metadata": {},
+ "execution_count": 15
+ }
+ ],
+ "source": [
+ "b = np.zeros(9, dtype=np.uint8)\n",
+ "\n",
+ "b[::2] = a\n",
+ "b[::2] += a\n",
+ "b[1::2] = a[:-1]\n",
+ "b[1::2] += a[1:]\n",
+ "\n",
+ "b"
+ ]
+ },
+ {
+ "source": [
+ "The results are the same, but no extra RAM is allocated, except for the views `a[:-1]`, and `a[1:]`, but those had to be created even in the origin implementation."
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "### Upscaling images\n",
+ "\n",
+ "And now the example: there are low-resolution thermal cameras out there. Low resolution might mean 8 by 8 pixels. Such a small number of pixels is just not reasonable to plot, no matter how small the display is. If you want to make the camera image a bit more pleasing, you can upscale (stretch) it in both dimensions. This can be done exactly as we up-scaled the linear array:"
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\n",
+ "b = np.zeros((15, 15), dtype=np.uint8)\n",
+ "\n",
+ "b[1::2,::2] = a[:-1,:]\n",
+ "b[1::2,::2] += a[1:, :]\n",
+ "b[1::2,::2] //= 2\n",
+ "b[::,1::2] = a[::,:-1:2]\n",
+ "b[::,1::2] += a[::,2::2]\n",
+ "b[::,1::2] //= 2"
+ ]
+ },
+ {
+ "source": [
+ "Up-scaling by larger numbers can be done in a similar fashion, you simply have more assignments."
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "There are cases, when one cannot do away with the intermediate results. Two prominent cases are the `where` function, and indexing by means of a Boolean array. E.g., in"
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "array([1, 2, 3])"
+ ]
+ },
+ "metadata": {},
+ "execution_count": 20
+ }
+ ],
+ "source": [
+ "a = array([1, 2, 3, 4, 5])\n",
+ "b = a[a < 4]\n",
+ "b"
+ ]
+ },
+ {
+ "source": [
+ "the expression `a < 4` produces the Boolean array, "
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "source": [
+ "a < 4"
+ ],
+ "cell_type": "code",
+ "metadata": {},
+ "execution_count": 22,
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "array([ True, True, True, False, False])"
+ ]
+ },
+ "metadata": {},
+ "execution_count": 22
+ }
+ ]
+ },
+ {
+ "source": [
+ "If you repeatedly have such conditions in a loop, you might have to peridically call the garbage collector to remove the Boolean arrays that are used only once."
+ ],
+ "cell_type": "markdown",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+} \ No newline at end of file
diff --git a/circuitpython/extmod/ulab/docs/ulab-utils.ipynb b/circuitpython/extmod/ulab/docs/ulab-utils.ipynb
new file mode 100644
index 0000000..4fddc53
--- /dev/null
+++ b/circuitpython/extmod/ulab/docs/ulab-utils.ipynb
@@ -0,0 +1,471 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-04T18:21:22.822563Z",
+ "start_time": "2021-03-04T18:21:18.656643Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Populating the interactive namespace from numpy and matplotlib\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pylab inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook magic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-05T06:53:22.506665Z",
+ "start_time": "2021-03-05T06:53:22.499658Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
+ "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
+ "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
+ "import subprocess\n",
+ "import os"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-05T06:53:23.127314Z",
+ "start_time": "2021-03-05T06:53:23.103181Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@magics_class\n",
+ "class PyboardMagic(Magics):\n",
+ " @cell_magic\n",
+ " @magic_arguments()\n",
+ " @argument('-skip')\n",
+ " @argument('-unix')\n",
+ " @argument('-pyboard')\n",
+ " @argument('-file')\n",
+ " @argument('-data')\n",
+ " @argument('-time')\n",
+ " @argument('-memory')\n",
+ " def micropython(self, line='', cell=None):\n",
+ " args = parse_argstring(self.micropython, line)\n",
+ " if args.skip: # doesn't care about the cell's content\n",
+ " print('skipped execution')\n",
+ " return None # do not parse the rest\n",
+ " if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
+ " with open('/dev/shm/micropython.py', 'w') as fout:\n",
+ " fout.write(cell)\n",
+ " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
+ " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
+ " print(proc.stdout.read().decode(\"utf-8\"))\n",
+ " print(proc.stderr.read().decode(\"utf-8\"))\n",
+ " return None\n",
+ " if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
+ " spaces = \" \"\n",
+ " try:\n",
+ " with open(args.file, 'w') as fout:\n",
+ " fout.write(cell.replace('\\t', spaces))\n",
+ " printf('written cell to {}'.format(args.file))\n",
+ " except:\n",
+ " print('Failed to write to disc!')\n",
+ " return None # do not parse the rest\n",
+ " if args.data: # can be used to load data from the pyboard directly into kernel space\n",
+ " message = pyb.exec(cell)\n",
+ " if len(message) == 0:\n",
+ " print('pyboard >>>')\n",
+ " else:\n",
+ " print(message.decode('utf-8'))\n",
+ " # register new variable in user namespace\n",
+ " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
+ " \n",
+ " if args.time: # measures the time of executions\n",
+ " pyb.exec('import utime')\n",
+ " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
+ " \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
+ " print(message.decode('utf-8'))\n",
+ " \n",
+ " if args.memory: # prints out memory information \n",
+ " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
+ " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ " message = pyb.exec(cell)\n",
+ " print(\">>> \", message.decode('utf-8'))\n",
+ " message = pyb.exec('print(mem_info())')\n",
+ " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
+ "\n",
+ " if args.pyboard:\n",
+ " message = pyb.exec(cell)\n",
+ " print(message.decode('utf-8'))\n",
+ "\n",
+ "ip = get_ipython()\n",
+ "ip.register_magics(PyboardMagic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## pyboard"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:35.126401Z",
+ "start_time": "2020-05-07T07:35:35.105824Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyboard\n",
+ "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
+ "pyb.enter_raw_repl()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-19T19:11:18.145548Z",
+ "start_time": "2020-05-19T19:11:18.137468Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "pyb.exit_raw_repl()\n",
+ "pyb.close()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2020-05-07T07:35:38.725924Z",
+ "start_time": "2020-05-07T07:35:38.645488Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -pyboard 1\n",
+ "\n",
+ "import utime\n",
+ "import ulab as np\n",
+ "\n",
+ "def timeit(n=1000):\n",
+ " def wrapper(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " run_times = np.zeros(n, dtype=np.uint16)\n",
+ " for i in range(n):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
+ " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
+ " print('\\tbest: %d us'%np.min(run_times))\n",
+ " print('\\tworst: %d us'%np.max(run_times))\n",
+ " print('\\taverage: %d us'%np.mean(run_times))\n",
+ " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n",
+ " return result\n",
+ " return new_func\n",
+ " return wrapper\n",
+ "\n",
+ "def timeit(f, *args, **kwargs):\n",
+ " func_name = str(f).split(' ')[1]\n",
+ " def new_func(*args, **kwargs):\n",
+ " t = utime.ticks_us()\n",
+ " result = f(*args, **kwargs)\n",
+ " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
+ " return result\n",
+ " return new_func"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "__END_OF_DEFS__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# ulab utilities\n",
+ "\n",
+ "\n",
+ "There might be cases, when the format of your data does not conform to `ulab`, i.e., there is no obvious way to map the data to any of the five supported `dtype`s. A trivial example is an ADC or microphone signal with 32-bit resolution. For such cases, `ulab` defines the `utils` module, which, at the moment, has four functions that are not `numpy` compatible, but which should ease interfacing `ndarray`s to peripheral devices. \n",
+ "\n",
+ "The `utils` module can be enabled by setting the `ULAB_HAS_UTILS_MODULE` constant to 1 in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h):\n",
+ "\n",
+ "```c\n",
+ "#ifndef ULAB_HAS_UTILS_MODULE\n",
+ "#define ULAB_HAS_UTILS_MODULE (1)\n",
+ "#endif\n",
+ "```\n",
+ "\n",
+ "This still does not compile any functions into the firmware. You can add a function by setting the corresponding pre-processor constant to 1. E.g., \n",
+ "\n",
+ "```c\n",
+ "#ifndef ULAB_UTILS_HAS_FROM_INT16_BUFFER\n",
+ "#define ULAB_UTILS_HAS_FROM_INT16_BUFFER (1)\n",
+ "#endif\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## from_int32_buffer, from_uint32_buffer\n",
+ "\n",
+ "With the help of `utils.from_int32_buffer`, and `utils.from_uint32_buffer`, it is possible to convert 32-bit integer buffers to `ndarrays` of float type. These functions have a syntax similar to `numpy.frombuffer`; they support the `count=-1`, and `offset=0` keyword arguments. However, in addition, they also accept `out=None`, and `byteswap=False`. \n",
+ "\n",
+ "Here is an example without keyword arguments"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-05T06:53:26.256516Z",
+ "start_time": "2021-03-05T06:53:26.007070Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: bytearray(b'\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\xff')\n",
+ "\n",
+ "unsigned integers: array([257.0, 4278190080.000001], dtype=float64)\n",
+ "\n",
+ "b: bytearray(b'\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\xff')\n",
+ "\n",
+ "signed integers: array([257.0, -16777216.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import utils\n",
+ "\n",
+ "a = bytearray([1, 1, 0, 0, 0, 0, 0, 255])\n",
+ "print('a: ', a)\n",
+ "print()\n",
+ "print('unsigned integers: ', utils.from_uint32_buffer(a))\n",
+ "\n",
+ "b = bytearray([1, 1, 0, 0, 0, 0, 0, 255])\n",
+ "print('\\nb: ', b)\n",
+ "print()\n",
+ "print('signed integers: ', utils.from_int32_buffer(b))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The meaning of `count`, and `offset` is similar to that in `numpy.frombuffer`. `count` is the number of floats that will be converted, while `offset` would discard the first `offset` number of bytes from the buffer before the conversion.\n",
+ "\n",
+ "In the example above, repeated calls to either of the functions returns a new `ndarray`. You can save RAM by supplying the `out` keyword argument with a pre-defined `ndarray` of sufficient size, in which case the results will be inserted into the `ndarray`. If the `dtype` of `out` is not `float`, a `TypeError` exception will be raised."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-05T06:53:41.551440Z",
+ "start_time": "2021-03-05T06:53:41.534163Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "b: bytearray(b'\\x01\\x00\\x01\\x00\\x00\\x01\\x00\\x01')\n",
+ "a: array([65537.0, 16777472.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import utils\n",
+ "\n",
+ "a = np.array([1, 2], dtype=np.float)\n",
+ "b = bytearray([1, 0, 1, 0, 0, 1, 0, 1])\n",
+ "print('b: ', b)\n",
+ "utils.from_uint32_buffer(b, out=a)\n",
+ "print('a: ', a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, since there is no guarantee that the endianness of a particular peripheral device supplying the buffer is the same as that of the microcontroller, `from_(u)intbuffer` allows a conversion via the `byteswap` keyword argument."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2021-03-05T06:53:52.242950Z",
+ "start_time": "2021-03-05T06:53:52.229160Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a: bytearray(b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x01')\n",
+ "buffer without byteswapping: array([1.0, 16777216.0], dtype=float64)\n",
+ "buffer with byteswapping: array([16777216.0, 1.0], dtype=float64)\n",
+ "\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%micropython -unix 1\n",
+ "\n",
+ "from ulab import numpy as np\n",
+ "from ulab import utils\n",
+ "\n",
+ "a = bytearray([1, 0, 0, 0, 0, 0, 0, 1])\n",
+ "print('a: ', a)\n",
+ "print('buffer without byteswapping: ', utils.from_uint32_buffer(a))\n",
+ "print('buffer with byteswapping: ', utils.from_uint32_buffer(a, byteswap=True))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## from_int16_buffer, from_uint16_buffer\n",
+ "\n",
+ "These two functions are identical to `utils.from_int32_buffer`, and `utils.from_uint32_buffer`, with the exception that they convert 16-bit integers to floating point `ndarray`s. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.5"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {
+ "height": "calc(100% - 180px)",
+ "left": "10px",
+ "top": "150px",
+ "width": "382.797px"
+ },
+ "toc_section_display": true,
+ "toc_window_display": true
+ },
+ "varInspector": {
+ "cols": {
+ "lenName": 16,
+ "lenType": 16,
+ "lenVar": 40
+ },
+ "kernels_config": {
+ "python": {
+ "delete_cmd_postfix": "",
+ "delete_cmd_prefix": "del ",
+ "library": "var_list.py",
+ "varRefreshCmd": "print(var_dic_list())"
+ },
+ "r": {
+ "delete_cmd_postfix": ") ",
+ "delete_cmd_prefix": "rm(",
+ "library": "var_list.r",
+ "varRefreshCmd": "cat(var_dic_list()) "
+ }
+ },
+ "types_to_exclude": [
+ "module",
+ "function",
+ "builtin_function_or_method",
+ "instance",
+ "_Feature"
+ ],
+ "window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}