aboutsummaryrefslogtreecommitdiff
path: root/circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst
diff options
context:
space:
mode:
Diffstat (limited to 'circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst')
-rw-r--r--circuitpython/extmod/ulab/docs/manual/source/ulab-ndarray.rst2607
1 files changed, 2607 insertions, 0 deletions
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.