circuitpython-ulab/docs/ulab-manual.ipynb
2019-10-15 20:03:13 +02:00

2233 lines
68 KiB
Text

{
"cells": [
{
"cell_type": "code",
"execution_count": 38,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T04:58:51.963087Z",
"start_time": "2019-10-11T04:58:49.955560Z"
}
},
"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 conversion"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:15:22.952255Z",
"start_time": "2019-10-10T17:15:19.094337Z"
}
},
"outputs": [],
"source": [
"import nbformat as nb\n",
"import nbformat.v4.nbbase as nb4\n",
"from nbconvert import RSTExporter\n",
"\n",
"def convert_notebook(nbfile, rstfile):\n",
" (rst, resources) = rstexporter.from_filename(nbfile)\n",
" with open(rstfile, 'w') as fout:\n",
" fout.write(rst)\n",
" \n",
"rstexporter = RSTExporter()\n",
"rstexporter.template_file = './templates/rst.tpl'\n",
"\n",
"convert_notebook('ulab-manual.ipynb', './source/ulab-manual.rst')"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:15:22.996213Z",
"start_time": "2019-10-10T17:15:22.988054Z"
}
},
"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": 11,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:15:23.096584Z",
"start_time": "2019-10-10T17:15:23.011929Z"
}
},
"outputs": [],
"source": [
"@magics_class\n",
"class PyboardMagic(Magics):\n",
" @cell_magic\n",
" @magic_arguments()\n",
" @argument('-skip')\n",
" @argument('-unix')\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",
" else:\n",
" message = pyb.exec(cell)\n",
" print(message.decode('utf-8'))\n",
"\n",
"ip = get_ipython()\n",
"ip.register_magics(PyboardMagic)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pyboard\n",
"pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
"pyb.enter_raw_repl()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pyb.exit_raw_repl()\n",
"pyb.close()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"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=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 timeit"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Introduction"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In https://micropython-usermod.readthedocs.io/en/latest/usermods_14.html, I mentioned that I have another story in store, for another day. The day has come, so here is my story.\n",
"\n",
"## ulab\n",
"\n",
"`ulab` is a numpy-like module for micropython, meant to simplify and speed up common mathematical operations on. The primary goal was to implement a small subset of numpy that might be useful in the context of a microcontroller. This means low-level data processing of linear (array) and two-dimensional (matrix) data.\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 the ADC signal, and all the 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, 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 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.\n",
"\n",
"The main points of `ulab` are \n",
"\n",
"- compact, iterable and slicable container of numerical data in 1, and 2 dimensions (arrays and matrices). These containers support all the relevant unary and binary operators (e.g., `len`, ==, +, *, etc.)\n",
"- vectorised computations on micropython iterables and numerical arrays/matrices (in numpy-speak, universal functions)\n",
"- basic linear algebra routines (matrix inversion, multiplication, reshaping, and transposition)\n",
"- polynomial fits to numerical data\n",
"- fast Fourier transforms\n",
"\n",
"At the time of writing this manual (for version 0.17), the library adds approximately 23 kB of extra compiled code to the firmware. \n",
"\n",
"## Resources and legal matters\n",
"\n",
"The source code can be found under https://github.com/v923z/micropython-ulab/code/. The source of this user manual is under https://github.com/v923z/micropython-ulab/docs/manual/, while the technical details of the implementation are discussed at great length in https://github.com/v923z/micropython-ulab/docs/ulab.ipynb. If you want an even thorougher explanation on why the various constructs of the implementation work, and work in that particular way, you can read more on the subject under https://micropython-usermod.readthedocs.io/en/latest/, where I demonstrate, what you have to do, if you want to make a C object behave in a *pythonic* way. \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 an issue on github, so that the community can profit from your experiences. \n",
"\n",
"Even better, if you find the project useful, and think that it could be made better, faster, smaller, 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 me know, so that *we* can fix it."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ndarray, the basic container\n",
"\n",
"The `ndarray` is the underlying container of numerical data. It is derived from micropython's own `array` object, but has a great number of extra features starting with how it can be initialised, how operations can be done on it, and which functions can accept it as an argument.\n",
"\n",
"Since `ndarray`s are a binary container, they are also compact, meaning that they take 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 possible type. 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 bytes per datum. \n",
"\n",
"On the following pages, we will see how one can work with `ndarray`. Those familiar with `numpy` should find that the nomenclature and naming conventions of `numpy` are adhered to as closely as possible. I will point out the few differences, where necessary.\n",
"\n",
"Hint: you can easily port existing `numpy` code, if you `import ulab as np`."
]
},
{
"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 is 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. "
]
},
{
"cell_type": "code",
"execution_count": 174,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T17:43:37.591149Z",
"start_time": "2019-10-11T17:43:37.571853Z"
}
},
"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=float)\n",
"\n",
"c:\t array([[0, 1, 2, 3, 4],\n",
"\t [20, 21, 22, 23, 24],\n",
"\t [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",
"import ulab 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."
]
},
{
"cell_type": "code",
"execution_count": 175,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T17:46:37.653805Z",
"start_time": "2019-10-11T17:46:37.641837Z"
}
},
"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=float)\n",
"\n",
"c:\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",
"\n",
"a = [1, 2, 3, 4, 5, 6, 7, 8]\n",
"b = np.array(a)\n",
"c = np.array(b)\n",
"\n",
"print(\"a:\\t\", a)\n",
"print(\"b:\\t\", b)\n",
"print(\"\\nc:\\t\", c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Methods of ndarrays"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### .asbytearray()\n",
"\n",
"The contents of an `ndarray` can be accessed directly by calling the `.asbytearray` method. This will return a pointer to the underlying `array` object, which can then be manipulated directly."
]
},
{
"cell_type": "code",
"execution_count": 237,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-12T07:02:24.016258Z",
"start_time": "2019-10-12T07:02:24.005080Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array content: array('b', [1, 2, 3, 4])\n",
"array content: array('b', [1, 123, 3, 4])\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
"buffer = a.asbytearray()\n",
"print(\"array content:\", buffer)\n",
"buffer[1] = 123\n",
"print(\"array content:\", buffer)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This in itself wouldn't be very interesting, but since `buffer` is a proper micropython `array`, we can pass it to functions that can employ the buffer protocol. E.g., all the `ndarray` facilities can be applied to the results of timed ADC conversions."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%micropython -unix 1\n",
"\n",
"import pyb\n",
"import ulab as np\n",
"\n",
"n = 100\n",
"\n",
"adc = pyb.ADC(pyb.Pin.board.X19)\n",
"tim = pyb.Timer(6, freq=10)\n",
"\n",
"a = np.array([0]*n, dtype=np.uint8)\n",
"buffer = a.asbytearray()\n",
"adc.read_timed(buf, tim)\n",
"\n",
"print(\"ADC results:\\t\", a)\n",
"print(\"mean of results:\\t\", np.mean(a))\n",
"print(\"std of results:\\t\", np.std(a))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Likewise, data can be read directly into `ndarray`s from other interfaces, e.g., SPI, I2C etc. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### .flatten()\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flatten.htm"
]
},
{
"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, and returns either the length (for row vectors), or the number of rows (for matrices) of its argument."
]
},
{
"cell_type": "code",
"execution_count": 105,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T13:23:54.932077Z",
"start_time": "2019-10-11T13:23:54.911337Z"
}
},
"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: (1, 5)\n",
"\n",
"b:\t array([[0, 1, 2, 3, 4],\n",
"\t [0, 1, 2, 3, 4],\n",
"\t [0, 1, 2, 3, 4],\n",
"\t [0, 1, 2, 3, 4]], dtype=uint8)\n",
"length of b: 4\n",
"shape of b: (4, 5)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab 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 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",
"import ulab 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",
"import ulab 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",
"import ulab 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",
"import ulab 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 operations\n",
"\n",
"All binary operators work element-wise. This also means that the operands either must have the same shape, or one of them must be a scalar.\n",
"\n",
"**WARNING:** `numpy` also allows operations between a matrix, and a row vector, if the row vector has exactly as many elements, as many columns the matrix has. This feature will be added in future versions of `ulab`."
]
},
{
"cell_type": "code",
"execution_count": 188,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T18:59:35.810060Z",
"start_time": "2019-10-11T18:59:35.804683Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"array([[11, 22, 33],\n",
" [14, 25, 36],\n",
" [17, 28, 36]])"
]
},
"execution_count": 188,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"a = array([[1, 2, 3], [4, 5, 6], [7, 8, 6]])\n",
"b = array([10, 20, 30])\n",
"a+b"
]
},
{
"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 that of `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 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 is automatically a float\n",
"\n",
"3. \n",
" \n",
"| left hand side | right hand side | result |\n",
"|----------------|-----------------|--------|\n",
"|`uint8` |`int8` |`int16` |\n",
"|`uint8` |`int16` |`int16` |\n",
"|`uint8` |`uint16` |`uint16`|\n",
"|`int8` |`int16` |`int16` |\n",
"|`int8` |`uint16` |`uint16`|\n",
"|`uint16` |`int16` |`float` |\n",
" \n",
"Note that the last two operations are promoted to `int32` in `numpy`.\n",
" \n",
"4. When the right hand side of a binary operator is a micropython variable, `mp_obj_int`, or `mp_obj_float`, then the result will be promoted to `dtype` `float`. This is necessary, because a micropython integer can be 31 bites wide. Other micropython types (e.g., lists, tuples, etc.) raise a `TypeError` exception. \n",
"\n",
"**WARNING:** Due to the lower number of available data types, the upcasting rules of `ulab` are slightly different to those of `numpy`. Keep this in mind, when porting code!\n",
"\n",
"Upcasting can be seen in action in the following snippet:"
]
},
{
"cell_type": "code",
"execution_count": 212,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T19:28:50.688741Z",
"start_time": "2019-10-11T19:28:50.674459Z"
}
},
"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=float)\n",
"a*c:\t array([1.0, 4.0, 9.0, 16.0], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab 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": [
"**WARNING:** If a binary operation involves an `ndarray` and a micropython type (integer, or float), then the array must be on the left hand side. "
]
},
{
"cell_type": "code",
"execution_count": 181,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T18:38:30.369710Z",
"start_time": "2019-10-11T18:38:30.354603Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a:\t array([1, 2, 3, 4], dtype=uint8)\n",
"b:\t 12\n",
"a+b:\t array([13, 14, 15, 16], dtype=uint8)\n",
"\n",
"Traceback (most recent call last):\n",
" File \"/dev/shm/micropython.py\", line 12, in <module>\n",
"TypeError: unsupported types for __add__: 'int', 'ndarray'\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"# this is going to work\n",
"a = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
"b = 12\n",
"print(\"a:\\t\", a)\n",
"print(\"b:\\t\", b)\n",
"print(\"a+b:\\t\", a+b)\n",
"\n",
"# but this will spectacularly fail\n",
"print(\"b+a:\\t\", b+a)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The reason for this lies in how micropython resolves binary operators, and this means that a fix can only be implemented, if micropython itself changes the corresponding function(s). Till then, keep `ndarray`s on the left hand side. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Benchmarks\n",
"\n",
"The following snippet compares the performance of binary operations to a possible implementation in python."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"@timeit\n",
"def py_add(a, b):\n",
" return [a[i]+b[i] for i in range(len(a))]\n",
"\n",
"@timeit\n",
"def py_multiply(a, b):\n",
" return [a[i]*b[i] for i in range(len(a))]\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",
"py_add(a, b)\n",
"\n",
"py_multiply(a, b)\n",
"\n",
"a = np.linspace(0, 10, 1000)\n",
"b = np.ones(1000)\n",
"\n",
"ulab_add(a, b)\n",
"\n",
"ulab_multiply(a, b)"
]
},
{
"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": 118,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T16:24:11.562136Z",
"start_time": "2019-10-11T16:24:11.548252Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[True, True, True, True, False, False, False, False]\n",
"\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(a < 5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**WARNING:** Note that `numpy` returns an array of Booleans. For most use cases this fact should not make a difference. "
]
},
{
"cell_type": "code",
"execution_count": 119,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T16:25:30.888882Z",
"start_time": "2019-10-11T16:25:30.865641Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"array([ True, True, True, True, False, False, False, False])"
]
},
"execution_count": 119,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"a = array([1, 2, 3, 4, 5, 6, 7, 8])\n",
"a < 5"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"These operators work with matrices, too, in which case a list of lists of Booleans will be returned:"
]
},
{
"cell_type": "code",
"execution_count": 122,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T16:28:07.876371Z",
"start_time": "2019-10-11T16:28:07.859304Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array([[0, 1, 2, 3, 4],\n",
"\t [1, 2, 3, 4, 5],\n",
"\t [2, 3, 4, 5, 6]], dtype=uint8)\n",
"[[True, True, True, True, True], [True, True, True, True, False], [True, True, True, False, False]]\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"a = np.array([range(0, 5, 1), range(1, 6, 1), range(2, 7, 1)], dtype=np.uint8)\n",
"print(a)\n",
"print(a < 5)"
]
},
{
"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 one-dimensional `ndarray`, which is simply a copy of the corresponding row of the matrix, i.e, its data type will be inherited."
]
},
{
"cell_type": "code",
"execution_count": 104,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T13:23:32.570237Z",
"start_time": "2019-10-11T13:23:32.550470Z"
}
},
"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",
"\t [10, 11, 12, 13, 14],\n",
"\t [20, 21, 22, 23, 24],\n",
"\t [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",
"import ulab 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",
"Copies of sub-arrays can be created by indexing, and slicing."
]
},
{
"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": 116,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T15:58:24.845241Z",
"start_time": "2019-10-11T15:58:24.821490Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a:\t\t\t\t\t\t array([0, 1, 2, ..., 7, 8, 9], dtype=uint8)\n",
"the first, and first from right element of a:\t 0 9\n",
"the second, and second from right element of a:\t 1 8\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"a = np.array(range(10), dtype=np.uint8)\n",
"print(\"a:\\t\\t\\t\\t\\t\\t\", a)\n",
"print(\"the first, and first from right element of a:\\t\", a[0], a[-1])\n",
"print(\"the second, and second from right element of a:\\t\", a[1], a[-2])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Indices are (not necessarily non-negative) integers, or a list of Booleans. By using 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, for matrices the function raises a `ValueError` exception, though this will be rectified in a future version of `ulab`."
]
},
{
"cell_type": "code",
"execution_count": 183,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T18:51:18.226269Z",
"start_time": "2019-10-11T18:51:18.208328Z"
}
},
"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",
"import ulab 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": 184,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T18:51:24.282953Z",
"start_time": "2019-10-11T18:51:24.259474Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a:\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
"a*2:\t array([0, 1, 4, 9, 16, 25, 36, 49, 64], dtype=uint8)\n",
"b:\t array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=uint8)\n",
"100*sin(b):\t array([-75.68025207519532, -75.68025207519532, -75.68025207519532, 14.11200046539307, 14.11200046539307, 14.11200046539307, 42.01670455932617, 42.01670455932617, 42.01670455932617], dtype=float)\n",
"a < b:\t array([0, 1, 2, 4, 5, 7, 8], dtype=uint8)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab 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(\"a*2:\\t\", a*a)\n",
"print(\"b:\\t\", b)\n",
"print(\"100*sin(b):\\t\", np.sin(b)*100.0)\n",
"print(\"a < b:\\t\", a[a*a > np.sin(b)*100.0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Slicing\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 \n",
"\n",
"```python\n",
"slice = start:end:stop\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."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Universal functions\n",
"\n",
"Standard mathematical functions can be calculated on any iterable, 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 functions execute faster with `ndarray` arguments than with iterables, because the values of the input vector can be extracted faster. "
]
},
{
"cell_type": "code",
"execution_count": 167,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-11T17:18:43.685083Z",
"start_time": "2019-10-11T17:18:43.662939Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a:\t range(0, 9)\n",
"exp(a):\t array([1.0, 2.718281745910645, 7.389056205749512, 20.08553695678711, 54.59814834594727, 148.4131622314453, 403.4288024902343, 1096.633178710938, 2980.9580078125], dtype=float)\n",
"\n",
"b:\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
"exp(b):\t array([1.0, 2.718281745910645, 7.389056205749512, 20.08553695678711, 54.59814834594727, 148.4131622314453, 403.4288024902343, 1096.633178710938, 2980.9580078125], dtype=float)\n",
"\n",
"c:\t 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",
"exp(c):\t array([[2.718281745910645, 7.389056205749512, 20.08553695678711],\n",
"\t [54.59814834594727, 148.4131622314453, 403.4288024902343],\n",
"\t [1096.633178710938, 2980.9580078125, 8103.083984375001]], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab 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('\\nb:\\t', b)\n",
"print('exp(b):\\t', np.exp(b))\n",
"\n",
"# as well as with matrices\n",
"c = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
"print('\\nc:\\t', c)\n",
"print('exp(c):\\t', np.exp(c))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The overhead for iterables is"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"a = [0]*1000\n",
"b = np.array(a)\n",
"\n",
"def measure_run_time(x):\n",
" return np.exp(x)\n",
"\n",
"@timeit(n=1000)\n",
"measure_run_time(a)\n",
"\n",
"@timeit(n=1000)\n",
"measure_run_time(b)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Of course, such a time saving is reasonable only, if the data are already available as an `ndarray`. If one has to initialise the `ndarray` from the list, then there is no gain, because the iterator was simple pushed into the initialisation function."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Numerical"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## linspace\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## argmin, argmax\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmin.html\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
"\n",
"Difference to `numpy`: the `out` keyword argument is not implemented."
]
},
{
"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",
"\n",
"a = np.array([1, 2, 3, 4, 5, 6, 7, 8])\n",
"print(\"a:\\t\\t\\t\", a)\n",
"\n",
"np.roll(a, 2)\n",
"print(\"a rolled to the left:\\t\", a)\n",
"\n",
"# this should be the original vector\n",
"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": 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",
"\n",
"a = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])\n",
"print(\"a:\\n\", a)\n",
"\n",
"np.roll(a, 2)\n",
"print(\"\\na rolled to the left:\\n\", a)\n",
"\n",
"np.roll(a, -1, axis=1)\n",
"print(\"\\na rolled up:\\n\", a)\n",
"\n",
"np.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",
"\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 = np.exp([1, 2, 3, 4, 5])\n",
"weight = weight/np.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(np.mean(samples[-5:]*weight))\n",
" print(samples[-5:])\n",
" # the data are shifted by one position to the left\n",
" np.roll(samples, 1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Linalg"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## transpose\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.transpose.html\n",
"\n",
"Note that only square matrices can be transposed in place, and in general, an internal copy of the matrix is required. "
]
},
{
"cell_type": "code",
"execution_count": 261,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-15T04:58:12.087978Z",
"start_time": "2019-10-15T04:58:12.067256Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a: array([[1, 2, 3],\n",
"\t [4, 5, 6],\n",
"\t [7, 8, 9]], dtype=uint8)\n",
"transpose of a: array([[1, 4, 7],\n",
"\t [2, 5, 8],\n",
"\t [3, 6, 9]], dtype=uint8)\n",
"\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, 9]], dtype=np.uint8)\n",
"print(\"a: \", a)\n",
"a.transpose()\n",
"print(\"transpose of a: \", a)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## reshape\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html"
]
},
{
"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 2-tuple."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:15:39.366695Z",
"start_time": "2019-10-10T17:15:39.344152Z"
}
},
"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",
"\t [0.0, 0.0, 0.0, 0.0],\n",
"\t [0.0, 0.0, 0.0, 0.0],\n",
"\t [0.0, 0.0, 0.0, 0.0],\n",
"\t [0.0, 0.0, 0.0, 0.0],\n",
"\t [0.0, 0.0, 0.0, 0.0]], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"print(np.ones(6, dtype=np.uint8))\n",
"print(np.zeros((6, 4)))"
]
},
{
"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 `N` rows, and `M` columns), whether the ones are in the main diagonal (`k=0`), or in another diagonal that is shifted by `k` positions. Here are a couple of examples."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### With a single argument"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:22:21.351732Z",
"start_time": "2019-10-10T17:22:21.336206Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array([[1.0, 0.0, 0.0, 0.0, 0.0],\n",
"\t [0.0, 1.0, 0.0, 0.0, 0.0],\n",
"\t [0.0, 0.0, 1.0, 0.0, 0.0],\n",
"\t [0.0, 0.0, 0.0, 1.0, 0.0],\n",
"\t [0.0, 0.0, 0.0, 0.0, 1.0]], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"print(np.eye(5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Specifying the dimensions of the matrix"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:19:08.627749Z",
"start_time": "2019-10-10T17:19:08.612026Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array([[1, 0, 0, 0],\n",
"\t [0, 1, 0, 0],\n",
"\t [0, 0, 1, 0],\n",
"\t [0, 0, 0, 1],\n",
"\t [0, 0, 0, 0],\n",
"\t [0, 0, 0, 0]], dtype=int8)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"print(np.eye(4, M=6, dtype=np.int8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Shifting the diagonal"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:19:31.618997Z",
"start_time": "2019-10-10T17:19:31.600377Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array([[0, 0, 0, 0],\n",
"\t [1, 0, 0, 0],\n",
"\t [0, 1, 0, 0],\n",
"\t [0, 0, 1, 0],\n",
"\t [0, 0, 0, 1],\n",
"\t [0, 0, 0, 0]], dtype=int16)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"print(np.eye(4, M=6, k=-1, dtype=np.int16))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## inv\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, and raises a `ValueError` exception, if the matrix turns out to be singular."
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:27:14.072594Z",
"start_time": "2019-10-10T17:27:14.057606Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array([[-2.000000238418579, 2.5, -1.0],\n",
"\t [2.000000238418579, -4.0, 2.0],\n",
"\t [-0.3333334922790527, 1.833333373069763, -1.0]], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"m = np.array([[1, 2, 3], [4, 5, 6], [7, 8.5, 9]])\n",
"\n",
"print(np.inv(m))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that the cost of inverting a matrix is approximately is twice as many floats (RAM), as the number of entries in the original matrix, and approximately as many operations, as the number of entries."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## dot\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html\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": 249,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-12T20:08:40.277675Z",
"start_time": "2019-10-12T20:08:40.235042Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"m:\t array([[1, 2, 3],\n",
"\t [4, 5, 6],\n",
"\t [7, 10, 9]], dtype=uint8)\n",
"m^-1:\t array([[-1.25000011920929, 1.0, -0.25],\n",
"\t [0.5000000596046448, -1.0, 0.5],\n",
"\t [0.4166666269302368, 0.3333333432674408, -0.25]], dtype=float)\n",
"m*m^-1:\t array([[0.9999998807907104, 0.0, 0.0],\n",
"\t [-4.76837158203125e-07, 1.0, 0.0],\n",
"\t [-9.5367431640625e-07, 0.0, 1.0]], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"m = np.array([[1, 2, 3], [4, 5, 6], [7, 10, 9]], dtype=np.uint8)\n",
"n = np.inv(m)\n",
"print(\"m:\\t\", m)\n",
"print(\"m^-1:\\t\", n)\n",
"# this should be the unit matrix\n",
"print(\"m*m^-1:\\t\", 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": 37,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-10T17:33:17.921324Z",
"start_time": "2019-10-10T17:33:17.900587Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"array([[1, 2, 3, 4],\n",
"\t [5, 6, 7, 8]], dtype=uint8)\n",
"array([[1, 2],\n",
"\t [3, 4],\n",
"\t [5, 6],\n",
"\t [7, 8]], dtype=uint8)\n",
"array([[7.0, 10.0],\n",
"\t [23.0, 34.0]], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab 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": [
"## det\n",
"\n",
"numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.det.html\n",
"\n",
"The `det` function takes a single argument, and calculates the determinant of a square matrix that is not singular. The calculation is based on successive elimination of the matrix elements, and the function raises a `ValueError` exception, if the matrix is singular.\n",
"\n",
"**WARNING:** this function is to be implemented."
]
},
{
"cell_type": "markdown",
"metadata": {},
"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. It 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.\n",
"\n",
"**WARNING:** this function is to be implemented."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Polynomials"
]
},
{
"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": 253,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-12T20:25:02.456483Z",
"start_time": "2019-10-12T20:25:02.431013Z"
}
},
"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",
"\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": [
"## 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": 259,
"metadata": {
"ExecuteTime": {
"end_time": "2019-10-12T20:30:41.878735Z",
"start_time": "2019-10-12T20:30:41.864963Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"independent values:\t array([-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.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.00000011920929, 0.0, 0.0], 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([0.9999995231628418, -6.0, 8.999998092651367], dtype=float)\n",
"\n",
"\n"
]
}
],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab as np\n",
"\n",
"x = np.array([-3, -2, -1, 0, 1, 2, 3])\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": [
"# Fourier transforms"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%micropython -unix 1\n",
"\n",
"import ulab 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(y)\n",
"\n",
"a, b = np_fft(y)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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. \n",
"\n",
"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 a factor of more than four improvement. "
]
}
],
"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.7.3"
},
"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": 2
}