{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T18:54:58.722373Z", "start_time": "2021-01-13T18:54:57.178438Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "source": [ "%pylab inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Notebook magic" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T18:55:01.909310Z", "start_time": "2021-01-13T18:55:01.903634Z" } }, "outputs": [], "source": [ "from IPython.core.magic import Magics, magics_class, line_cell_magic\n", "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n", "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n", "import subprocess\n", "import os" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T18:55:02.434518Z", "start_time": "2021-01-13T18:55:02.382296Z" } }, "outputs": [], "source": [ "@magics_class\n", "class PyboardMagic(Magics):\n", " @cell_magic\n", " @magic_arguments()\n", " @argument('-skip')\n", " @argument('-unix')\n", " @argument('-pyboard')\n", " @argument('-file')\n", " @argument('-data')\n", " @argument('-time')\n", " @argument('-memory')\n", " def micropython(self, line='', cell=None):\n", " args = parse_argstring(self.micropython, line)\n", " if args.skip: # doesn't care about the cell's content\n", " print('skipped execution')\n", " return None # do not parse the rest\n", " if args.unix: # tests the code on the unix port. Note that this works on unix only\n", " with open('/dev/shm/micropython.py', 'w') as fout:\n", " fout.write(cell)\n", " proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n", " stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n", " print(proc.stdout.read().decode(\"utf-8\"))\n", " print(proc.stderr.read().decode(\"utf-8\"))\n", " return None\n", " if args.file: # can be used to copy the cell content onto the pyboard's flash\n", " spaces = \" \"\n", " try:\n", " with open(args.file, 'w') as fout:\n", " fout.write(cell.replace('\\t', spaces))\n", " printf('written cell to {}'.format(args.file))\n", " except:\n", " print('Failed to write to disc!')\n", " return None # do not parse the rest\n", " if args.data: # can be used to load data from the pyboard directly into kernel space\n", " message = pyb.exec(cell)\n", " if len(message) == 0:\n", " print('pyboard >>>')\n", " else:\n", " print(message.decode('utf-8'))\n", " # register new variable in user namespace\n", " self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n", " \n", " if args.time: # measures the time of executions\n", " pyb.exec('import utime')\n", " message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n", " \"\\nprint('execution time: {:d} us'.format(delta))\")\n", " print(message.decode('utf-8'))\n", " \n", " if args.memory: # prints out memory information \n", " message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n", " print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n", " message = pyb.exec(cell)\n", " print(\">>> \", message.decode('utf-8'))\n", " message = pyb.exec('print(mem_info())')\n", " print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n", "\n", " if args.pyboard:\n", " message = pyb.exec(cell)\n", " print(message.decode('utf-8'))\n", "\n", "ip = get_ipython()\n", "ip.register_magics(PyboardMagic)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## pyboard" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "ExecuteTime": { "end_time": "2020-05-07T07:35:35.126401Z", "start_time": "2020-05-07T07:35:35.105824Z" } }, "outputs": [], "source": [ "import pyboard\n", "pyb = pyboard.Pyboard('/dev/ttyACM0')\n", "pyb.enter_raw_repl()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "ExecuteTime": { "end_time": "2020-05-19T19:11:18.145548Z", "start_time": "2020-05-19T19:11:18.137468Z" } }, "outputs": [], "source": [ "pyb.exit_raw_repl()\n", "pyb.close()" ] }, { "cell_type": "code", "execution_count": 58, "metadata": { "ExecuteTime": { "end_time": "2020-05-07T07:35:38.725924Z", "start_time": "2020-05-07T07:35:38.645488Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "%%micropython -pyboard 1\n", "\n", "import utime\n", "import ulab as np\n", "\n", "def timeit(n=1000):\n", " def wrapper(f, *args, **kwargs):\n", " func_name = str(f).split(' ')[1]\n", " def new_func(*args, **kwargs):\n", " run_times = np.zeros(n, dtype=np.uint16)\n", " for i in range(n):\n", " t = utime.ticks_us()\n", " result = f(*args, **kwargs)\n", " run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n", " print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n", " print('\\tbest: %d us'%np.min(run_times))\n", " print('\\tworst: %d us'%np.max(run_times))\n", " print('\\taverage: %d us'%np.mean(run_times))\n", " print('\\tdeviation: +/-%.3f us'%np.std(run_times)) \n", " return result\n", " return new_func\n", " return wrapper\n", "\n", "def timeit(f, *args, **kwargs):\n", " func_name = str(f).split(' ')[1]\n", " def new_func(*args, **kwargs):\n", " t = utime.ticks_us()\n", " result = f(*args, **kwargs)\n", " print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n", " return result\n", " return new_func" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "__END_OF_DEFS__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Universal functions\n", "\n", "Standard mathematical functions can be calculated on any scalar, scalar-valued iterable (ranges, lists, tuples containing numbers), and on `ndarray`s without having to change the call signature. In all cases the functions return a new `ndarray` of typecode `float` (since these functions usually generate float values, anyway). The functions execute faster with `ndarray` arguments than with iterables, because the values of the input vector can be extracted faster. \n", "\n", "At present, the following functions are supported:\n", "\n", "`acos`, `acosh`, `arctan2`, `around`, `asin`, `asinh`, `atan`, `arctan2`, `atanh`, `ceil`, `cos`, `degrees`, `exp`, `expm1`, `floor`, `log`, `log10`, `log2`, `radians`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`.\n", "\n", "These functions are applied element-wise to the arguments, thus, e.g., the exponential of a matrix cannot be calculated in this way." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T19:11:07.579601Z", "start_time": "2021-01-13T19:11:07.554672Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "a:\t range(0, 9)\n", "exp(a):\t array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)\n", "\n", "=============\n", "b:\n", " array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n", "exp(b):\n", " array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)\n", "\n", "=============\n", "c:\n", " array([[0.0, 1.0, 2.0],\n", " [3.0, 4.0, 5.0],\n", " [6.0, 7.0, 8.0]], dtype=float64)\n", "exp(c):\n", " array([[1.0, 2.718281828459045, 7.38905609893065],\n", " [20.08553692318767, 54.59815003314424, 148.4131591025766],\n", " [403.4287934927351, 1096.633158428459, 2980.957987041728]], dtype=float64)\n", "\n", "\n" ] } ], "source": [ "%%micropython -unix 1\n", "\n", "from ulab import numpy as np\n", "\n", "a = range(9)\n", "b = np.array(a)\n", "\n", "# works with ranges, lists, tuples etc.\n", "print('a:\\t', a)\n", "print('exp(a):\\t', np.exp(a))\n", "\n", "# with 1D arrays\n", "print('\\n=============\\nb:\\n', b)\n", "print('exp(b):\\n', np.exp(b))\n", "\n", "# as well as with matrices\n", "c = np.array(range(9)).reshape((3, 3))\n", "print('\\n=============\\nc:\\n', c)\n", "print('exp(c):\\n', np.exp(c))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computation expenses\n", "\n", "The overhead for calculating with micropython iterables is quite significant: for the 1000 samples below, the difference is more than 800 microseconds, because internally the function has to create the `ndarray` for the output, has to fetch the iterable's items of unknown type, and then convert them to floats. All these steps are skipped for `ndarray`s, because these pieces of information are already known. \n", "\n", "Doing the same with `list` comprehension requires 30 times more time than with the `ndarray`, which would become even more, if we converted the resulting list to an `ndarray`. " ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "ExecuteTime": { "end_time": "2020-05-07T07:35:45.696282Z", "start_time": "2020-05-07T07:35:45.629909Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "iterating over ndarray in ulab\r\n", "execution time: 441 us\r\n", "\r\n", "iterating over list in ulab\r\n", "execution time: 1266 us\r\n", "\r\n", "iterating over list in python\r\n", "execution time: 11379 us\r\n", "\n" ] } ], "source": [ "%%micropython -pyboard 1\n", "\n", "from ulab import numpy as np\n", "import math\n", "\n", "a = [0]*1000\n", "b = np.array(a)\n", "\n", "@timeit\n", "def timed_vector(iterable):\n", " return np.exp(iterable)\n", "\n", "@timeit\n", "def timed_list(iterable):\n", " return [math.exp(i) for i in iterable]\n", "\n", "print('iterating over ndarray in ulab')\n", "timed_vector(b)\n", "\n", "print('\\niterating over list in ulab')\n", "timed_vector(a)\n", "\n", "print('\\niterating over list in python')\n", "timed_list(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## arctan2\n", "\n", "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.arctan2.html\n", "\n", "The two-argument inverse tangent function is also part of the `vector` sub-module. The function implements broadcasting as discussed in the section on `ndarray`s. Scalars (`micropython` integers or floats) are also allowed." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T19:15:08.215912Z", "start_time": "2021-01-13T19:15:08.189806Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "a:\n", " array([1.0, 2.2, 33.33, 444.444], dtype=float64)\n", "\n", "arctan2(a, 1.0)\n", " array([0.7853981633974483, 1.14416883366802, 1.5408023243361, 1.568546328341769], dtype=float64)\n", "\n", "arctan2(1.0, a)\n", " array([0.7853981633974483, 0.426627493126876, 0.02999400245879636, 0.002249998453127392], dtype=float64)\n", "\n", "arctan2(a, a): \n", " array([0.7853981633974483, 0.7853981633974483, 0.7853981633974483, 0.7853981633974483], dtype=float64)\n", "\n", "\n" ] } ], "source": [ "%%micropython -unix 1\n", "\n", "from ulab import numpy as np\n", "\n", "a = np.array([1, 2.2, 33.33, 444.444])\n", "print('a:\\n', a)\n", "print('\\narctan2(a, 1.0)\\n', np.arctan2(a, 1.0))\n", "print('\\narctan2(1.0, a)\\n', np.arctan2(1.0, a))\n", "print('\\narctan2(a, a): \\n', np.arctan2(a, a))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## around\n", "\n", "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.around.html\n", "\n", "`numpy`'s `around` function can also be found in the `vector` sub-module. The function implements the `decimals` keyword argument with default value `0`. The first argument must be an `ndarray`. If this is not the case, the function raises a `TypeError` exception. Note that `numpy` accepts general iterables. The `out` keyword argument known from `numpy` is not accepted. The function always returns an ndarray of type `mp_float_t`." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T19:19:46.728823Z", "start_time": "2021-01-13T19:19:46.703348Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "a:\t\t array([1.0, 2.2, 33.33, 444.444], dtype=float64)\n", "\n", "decimals = 0\t array([1.0, 2.0, 33.0, 444.0], dtype=float64)\n", "\n", "decimals = 1\t array([1.0, 2.2, 33.3, 444.4], dtype=float64)\n", "\n", "decimals = -1\t array([0.0, 0.0, 30.0, 440.0], dtype=float64)\n", "\n", "\n" ] } ], "source": [ "%%micropython -unix 1\n", "\n", "from ulab import numpy as np\n", "\n", "a = np.array([1, 2.2, 33.33, 444.444])\n", "print('a:\\t\\t', a)\n", "print('\\ndecimals = 0\\t', np.around(a, decimals=0))\n", "print('\\ndecimals = 1\\t', np.around(a, decimals=1))\n", "print('\\ndecimals = -1\\t', np.around(a, decimals=-1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Vectorising generic python functions\n", "\n", "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html\n", "\n", "The examples above use factory functions. In fact, they are nothing but the vectorised versions of the standard mathematical functions. User-defined `python` functions can also be vectorised by help of `vectorize`. This function takes a positional argument, namely, the `python` function that you want to vectorise, and a non-mandatory keyword argument, `otypes`, which determines the `dtype` of the output array. The `otypes` must be `None` (default), or any of the `dtypes` defined in `ulab`. With `None`, the output is automatically turned into a float array. \n", "\n", "The return value of `vectorize` is a `micropython` object that can be called as a standard function, but which now accepts either a scalar, an `ndarray`, or a generic `micropython` iterable as its sole argument. Note that the function that is to be vectorised must have a single argument." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T19:16:55.709617Z", "start_time": "2021-01-13T19:16:55.688222Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "f on a scalar: array([1936.0], dtype=float64)\n", "f on an ndarray: array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n", "f on a list: array([4.0, 9.0, 16.0], dtype=float64)\n", "\n", "\n" ] } ], "source": [ "%%micropython -unix 1\n", "\n", "from ulab import numpy as np\n", "\n", "def f(x):\n", " return x*x\n", "\n", "vf = np.vectorize(f)\n", "\n", "# calling with a scalar\n", "print('{:20}'.format('f on a scalar: '), vf(44.0))\n", "\n", "# calling with an ndarray\n", "a = np.array([1, 2, 3, 4])\n", "print('{:20}'.format('f on an ndarray: '), vf(a))\n", "\n", "# calling with a list\n", "print('{:20}'.format('f on a list: '), vf([2, 3, 4]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As mentioned, the `dtype` of the resulting `ndarray` can be specified via the `otypes` keyword. The value is bound to the function object that `vectorize` returns, therefore, if the same function is to be vectorised with different output types, then for each type a new function object must be created." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "ExecuteTime": { "end_time": "2021-01-13T19:19:36.090837Z", "start_time": "2021-01-13T19:19:36.069088Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "output is uint8: array([1, 4, 9, 16], dtype=uint8)\n", "output is float: array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n", "\n", "\n" ] } ], "source": [ "%%micropython -unix 1\n", "\n", "from ulab import numpy as np\n", "\n", "l = [1, 2, 3, 4]\n", "def f(x):\n", " return x*x\n", "\n", "vf1 = np.vectorize(f, otypes=np.uint8)\n", "vf2 = np.vectorize(f, otypes=np.float)\n", "\n", "print('{:20}'.format('output is uint8: '), vf1(l))\n", "print('{:20}'.format('output is float: '), vf2(l))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `otypes` keyword argument cannot be used for type coercion: if the function evaluates to a float, but `otypes` would dictate an integer type, an exception will be raised:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "ExecuteTime": { "end_time": "2020-05-06T22:21:43.616220Z", "start_time": "2020-05-06T22:21:43.601280Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "integer list: array([1, 4, 9, 16], dtype=uint8)\n", "\n", "Traceback (most recent call last):\n", " File \"/dev/shm/micropython.py\", line 14, in \n", "TypeError: can't convert float to int\n", "\n" ] } ], "source": [ "%%micropython -unix 1\n", "\n", "from ulab import numpy as np\n", "\n", "int_list = [1, 2, 3, 4]\n", "float_list = [1.0, 2.0, 3.0, 4.0]\n", "def f(x):\n", " return x*x\n", "\n", "vf = np.vectorize(f, otypes=np.uint8)\n", "\n", "print('{:20}'.format('integer list: '), vf(int_list))\n", "# this will raise a TypeError exception\n", "print(vf(float_list))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Benchmarks\n", "\n", "It should be pointed out that the `vectorize` function produces the pseudo-vectorised version of the `python` function that is fed into it, i.e., on the C level, the same `python` function is called, with the all-encompassing `mp_obj_t` type arguments, and all that happens is that the `for` loop in `[f(i) for i in iterable]` runs purely in C. Since type checking and type conversion in `f()` is expensive, the speed-up is not so spectacular as when iterating over an `ndarray` with a factory function: a gain of approximately 30% can be expected, when a native `python` type (e.g., `list`) is returned by the function, and this becomes around 50% (a factor of 2), if conversion to an `ndarray` is also counted.\n", "\n", "The following code snippet calculates the square of a 1000 numbers with the vectorised function (which returns an `ndarray`), with `list` comprehension, and with `list` comprehension followed by conversion to an `ndarray`. For comparison, the execution time is measured also for the case, when the square is calculated entirely in `ulab`." ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "ExecuteTime": { "end_time": "2020-05-07T07:32:20.048553Z", "start_time": "2020-05-07T07:32:19.951851Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "vectorised function\r\n", "execution time: 7237 us\r\n", "\r\n", "list comprehension\r\n", "execution time: 10248 us\r\n", "\r\n", "list comprehension + ndarray conversion\r\n", "execution time: 12562 us\r\n", "\r\n", "squaring an ndarray entirely in ulab\r\n", "execution time: 560 us\r\n", "\n" ] } ], "source": [ "%%micropython -pyboard 1\n", "\n", "from ulab import numpy as np\n", "\n", "def f(x):\n", " return x*x\n", "\n", "vf = np.vectorize(f)\n", "\n", "@timeit\n", "def timed_vectorised_square(iterable):\n", " return vf(iterable)\n", "\n", "@timeit\n", "def timed_python_square(iterable):\n", " return [f(i) for i in iterable]\n", "\n", "@timeit\n", "def timed_ndarray_square(iterable):\n", " return np.array([f(i) for i in iterable])\n", "\n", "@timeit\n", "def timed_ulab_square(ndarray):\n", " return ndarray**2\n", "\n", "print('vectorised function')\n", "squares = timed_vectorised_square(range(1000))\n", "\n", "print('\\nlist comprehension')\n", "squares = timed_python_square(range(1000))\n", "\n", "print('\\nlist comprehension + ndarray conversion')\n", "squares = timed_ndarray_square(range(1000))\n", "\n", "print('\\nsquaring an ndarray entirely in ulab')\n", "a = np.array(range(1000))\n", "squares = timed_ulab_square(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From the comparisons above, it is obvious that `python` functions should only be vectorised, when the same effect cannot be gotten in `ulab` only. However, although the time savings are not significant, there is still a good reason for caring about vectorised functions. Namely, user-defined `python` functions become universal, i.e., they can accept generic iterables as well as `ndarray`s as their arguments. A vectorised function is still a one-liner, resulting in transparent and elegant code.\n", "\n", "A final comment on this subject: the `f(x)` that we defined is a *generic* `python` function. This means that it is not required that it just crunches some numbers. It has to return a number object, but it can still access the hardware in the meantime. So, e.g., \n", "\n", "```python\n", "\n", "led = pyb.LED(2)\n", "\n", "def f(x):\n", " if x < 100:\n", " led.toggle()\n", " return x*x\n", "```\n", "\n", "is perfectly valid code." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "382.797px" }, "toc_section_display": true, "toc_window_display": true }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }