aboutsummaryrefslogtreecommitdiff
path: root/circuitpython/extmod/ulab/docs/ulab-intro.ipynb
blob: 67d6b6088e2b8a589fc55307a6b55e6c3d31b13e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-08T12:07:55.382930Z",
     "start_time": "2021-01-08T12:07:46.895325Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Matplotlib is building the font cache; this may take a moment.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Populating the interactive namespace from numpy and matplotlib\n"
     ]
    }
   ],
   "source": [
    "%pylab inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Notebook magic"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2022-01-07T18:13:14.590799Z",
     "start_time": "2022-01-07T18:13:14.585845Z"
    }
   },
   "outputs": [],
   "source": [
    "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
    "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
    "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
    "import subprocess\n",
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2022-01-07T18:20:56.550047Z",
     "start_time": "2022-01-07T18:20:56.527475Z"
    }
   },
   "outputs": [],
   "source": [
    "@magics_class\n",
    "class PyboardMagic(Magics):\n",
    "    @cell_magic\n",
    "    @magic_arguments()\n",
    "    @argument('-skip')\n",
    "    @argument('-unix')\n",
    "    @argument('-pyboard')\n",
    "    @argument('-file')\n",
    "    @argument('-data')\n",
    "    @argument('-time')\n",
    "    @argument('-memory')\n",
    "    def micropython(self, line='', cell=None):\n",
    "        args = parse_argstring(self.micropython, line)\n",
    "        if args.skip: # doesn't care about the cell's content\n",
    "            print('skipped execution')\n",
    "            return None # do not parse the rest\n",
    "        if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
    "            with open('/dev/shm/micropython.py', 'w') as fout:\n",
    "                fout.write(cell)\n",
    "            proc = subprocess.Popen([\"../micropython/ports/unix/micropython-2\", \"/dev/shm/micropython.py\"], \n",
    "                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
    "            print(proc.stdout.read().decode(\"utf-8\"))\n",
    "            print(proc.stderr.read().decode(\"utf-8\"))\n",
    "            return None\n",
    "        if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
    "            spaces = \"    \"\n",
    "            try:\n",
    "                with open(args.file, 'w') as fout:\n",
    "                    fout.write(cell.replace('\\t', spaces))\n",
    "                    printf('written cell to {}'.format(args.file))\n",
    "            except:\n",
    "                print('Failed to write to disc!')\n",
    "            return None # do not parse the rest\n",
    "        if args.data: # can be used to load data from the pyboard directly into kernel space\n",
    "            message = pyb.exec(cell)\n",
    "            if len(message) == 0:\n",
    "                print('pyboard >>>')\n",
    "            else:\n",
    "                print(message.decode('utf-8'))\n",
    "                # register new variable in user namespace\n",
    "                self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
    "        \n",
    "        if args.time: # measures the time of executions\n",
    "            pyb.exec('import utime')\n",
    "            message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
    "                               \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
    "            print(message.decode('utf-8'))\n",
    "        \n",
    "        if args.memory: # prints out memory information \n",
    "            message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
    "            print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "            message = pyb.exec(cell)\n",
    "            print(\">>> \", message.decode('utf-8'))\n",
    "            message = pyb.exec('print(mem_info())')\n",
    "            print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "\n",
    "        if args.pyboard:\n",
    "            message = pyb.exec(cell)\n",
    "            print(message.decode('utf-8'))\n",
    "\n",
    "ip = get_ipython()\n",
    "ip.register_magics(PyboardMagic)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pyboard"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:35.126401Z",
     "start_time": "2020-05-07T07:35:35.105824Z"
    }
   },
   "outputs": [],
   "source": [
    "import pyboard\n",
    "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
    "pyb.enter_raw_repl()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-19T19:11:18.145548Z",
     "start_time": "2020-05-19T19:11:18.137468Z"
    }
   },
   "outputs": [],
   "source": [
    "pyb.exit_raw_repl()\n",
    "pyb.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:38.725924Z",
     "start_time": "2020-05-07T07:35:38.645488Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "import utime\n",
    "from ulab import numpy as np\n",
    "\n",
    "def timeit(n=1000):\n",
    "    def wrapper(f, *args, **kwargs):\n",
    "        func_name = str(f).split(' ')[1]\n",
    "        def new_func(*args, **kwargs):\n",
    "            run_times = np.zeros(n, dtype=np.uint16)\n",
    "            for i in range(n):\n",
    "                t = utime.ticks_us()\n",
    "                result = f(*args, **kwargs)\n",
    "                run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
    "            print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
    "            print('\\tbest: %d us'%np.min(run_times))\n",
    "            print('\\tworst: %d us'%np.max(run_times))\n",
    "            print('\\taverage: %d us'%np.mean(run_times))\n",
    "            print('\\tdeviation: +/-%.3f us'%np.std(run_times))            \n",
    "            return result\n",
    "        return new_func\n",
    "    return wrapper\n",
    "\n",
    "def timeit(f, *args, **kwargs):\n",
    "    func_name = str(f).split(' ')[1]\n",
    "    def new_func(*args, **kwargs):\n",
    "        t = utime.ticks_us()\n",
    "        result = f(*args, **kwargs)\n",
    "        print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
    "        return result\n",
    "    return new_func"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__END_OF_DEFS__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Introduction"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Enter ulab\n",
    "\n",
    "`ulab` is a `numpy`-like module for `micropython` and its derivatives, meant to simplify and speed up common mathematical operations on arrays. `ulab` implements a small subset of `numpy` and `scipy`. The functions were chosen such that they might be useful in the context of a microcontroller. However, the project is a living one, and suggestions for new features are always welcome. \n",
    "\n",
    "This document discusses how you can use the library, starting from building your own firmware, through questions like what affects the firmware size, what are the trade-offs, and what are the most important differences to `numpy` and `scipy`, respectively. The document is organised as follows:\n",
    "\n",
    "The chapter after this one helps you with firmware customisation.\n",
    "\n",
    "The third chapter gives a very concise summary of the `ulab` functions and array methods. This chapter can be used as a quick reference.\n",
    "\n",
    "The chapters after that are an in-depth review of most functions. Here you can find usage examples, benchmarks, as well as a thorough discussion of such concepts as broadcasting, and views versus copies. \n",
    "\n",
    "The final chapter of this book can be regarded as the programming manual. The inner working of `ulab` is dissected here, and you will also find hints as to how to implement your own `numpy`-compatible functions.\n",
    "\n",
    "\n",
    "## Purpose\n",
    "\n",
    "Of course, the first question that one has to answer is, why on Earth one would need a fast math library on a microcontroller. After all, it is not expected that heavy number crunching is going to take place on bare metal. It is not meant to. On a PC, the main reason for writing fast code is the sheer amount of data that one wants to process. On a microcontroller, the data volume is probably small, but it might lead to catastrophic system failure, if these data are not processed in time, because the microcontroller is supposed to interact with the outside world in a timely fashion. In fact, this latter objective was the initiator of this project: I needed the Fourier transform of a signal coming from the ADC of the `pyboard`, and all available options were simply too slow. \n",
    "\n",
    "In addition to speed, another issue that one has to keep in mind when working with embedded systems is the amount of available RAM: I believe, everything here could be implemented in pure `python` with relatively little effort (in fact, there are a couple of `python`-only implementations of `numpy` functions out there), but the price we would have to pay for that is not only speed, but RAM, too. `python` code, if is not frozen, and compiled into the firmware, has to be compiled at runtime, which is not exactly a cheap process. On top of that, if numbers are stored in a list or tuple, which would be the high-level container, then they occupy 8 bytes, no matter, whether they are all smaller than 100, or larger than one hundred million. This is obviously a waste of resources in an environment, where resources are scarce. \n",
    "\n",
    "Finally, there is a reason for using `micropython` in the first place. Namely, that a microcontroller can be programmed in a very elegant, and *pythonic* way. But if it is so, why should we not extend this idea to other tasks and concepts that might come up in this context? If there was no other reason than this *elegance*, I would find that convincing enough.\n",
    "\n",
    "Based on the above-mentioned considerations, all functions in `ulab` are implemented in a way that \n",
    "\n",
    "1. conforms to `numpy` as much as possible\n",
    "2. is so frugal with RAM as possible,\n",
    "3. and yet, fast. Much faster than pure python. Think of speed-ups of 30-50!\n",
    "\n",
    "The main points of `ulab` are \n",
    "\n",
    "- compact, iterable and slicable containers of numerical data in one to four dimensions. These containers support all the relevant unary and binary operators (e.g., `len`, ==, +, *, etc.)\n",
    "- vectorised computations on `micropython` iterables and numerical arrays (in `numpy`-speak, universal functions)\n",
    "- computing statistical properties (mean, standard deviation etc.) on arrays\n",
    "- basic linear algebra routines (matrix inversion, multiplication, reshaping, transposition, determinant, and eigenvalues, Cholesky decomposition and so on)\n",
    "- polynomial fits to numerical data, and evaluation of polynomials\n",
    "- fast Fourier transforms\n",
    "- filtering of data (convolution and second-order filters)\n",
    "- function minimisation, fitting, and numerical approximation routines\n",
    "- interfacing between numerical data and peripheral hardware devices\n",
    "\n",
    "`ulab` implements close to a hundred functions and array methods. At the time of writing this manual (for version 4.0.0), the library adds approximately 120 kB of extra compiled code to the `micropython` (pyboard.v.1.17) firmware. However, if you are tight with flash space, you can easily shave tens of kB off the firmware. In fact, if only a small sub-set of functions are needed, you can get away with less than 10 kB of flash space. See the section on [customising ulab](#Customising-the-firmware).\n",
    "\n",
    "## Resources and legal matters\n",
    "\n",
    "The source code of the module can be found under https://github.com/v923z/micropython-ulab/tree/master/code. while the source of this user manual is under https://github.com/v923z/micropython-ulab/tree/master/docs.\n",
    "\n",
    "The MIT licence applies to all material. \n",
    "\n",
    "## Friendly request\n",
    "\n",
    "If you use `ulab`, and bump into a bug, or think that a particular function is missing, or its behaviour does not conform to `numpy`, please, raise a [ulab issue](#https://github.com/v923z/micropython-ulab/issues) on github, so that the community can profit from your experiences. \n",
    "\n",
    "Even better, if you find the project to be useful, and think that it could be made better, faster, tighter, and shinier, please, consider contributing, and issue a pull request with the implementation of your improvements and new features. `ulab` can only become successful, if it offers what the community needs.\n",
    "\n",
    "These last comments apply to the documentation, too. If, in your opinion, the documentation is obscure, misleading, or not detailed enough, please, let us know, so that *we* can fix it.\n",
    "\n",
    "## Differences between micropython-ulab and circuitpython-ulab\n",
    "\n",
    "`ulab` has originally been developed for `micropython`, but has since been integrated into a number of its flavours. Most of these are simply forks of `micropython` itself, with some additional functionality. One of the notable exceptions is `circuitpython`, which has slightly diverged at the core level, and this has some minor consequences. Some of these concern the C implementation details only, which all have been sorted out with the generous and enthusiastic support of Jeff Epler from [Adafruit Industries](http://www.adafruit.com).\n",
    "\n",
    "There are, however, a couple of instances, where the two environments differ at the python level in how the class properties can be accessed. We will point out the differences and possible workarounds at the relevant places in this document."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Customising the firmware\n",
    "\n",
    "\n",
    "As mentioned above, `ulab` has considerably grown since its conception, which also means that it might no longer fit on the microcontroller of your choice. There are, however, a couple of ways of customising the firmware, and thereby reducing its size. \n",
    "\n",
    "All `ulab` options are listed in a single header file, [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h), which contains pre-processor flags for each feature that can be fine-tuned. The first couple of lines of the file look like this\n",
    "\n",
    "```c\n",
    "// The pre-processor constants in this file determine how ulab behaves:\n",
    "//\n",
    "// - how many dimensions ulab can handle\n",
    "// - which functions are included in the compiled firmware\n",
    "// - whether the python syntax is numpy-like, or modular\n",
    "// - whether arrays can be sliced and iterated over\n",
    "// - which binary/unary operators are supported\n",
    "//\n",
    "// A considerable amount of flash space can be saved by removing (setting\n",
    "// the corresponding constants to 0) the unnecessary functions and features.\n",
    "\n",
    "// Values defined here can be overridden by your own config file as\n",
    "// make -DULAB_CONFIG_FILE=\"my_ulab_config.h\"\n",
    "#if defined(ULAB_CONFIG_FILE)\n",
    "#include ULAB_CONFIG_FILE\n",
    "#endif\n",
    "\n",
    "// Adds support for complex ndarrays\n",
    "#ifndef ULAB_SUPPORTS_COMPLEX\n",
    "#define ULAB_SUPPORTS_COMPLEX               (1)\n",
    "#endif\n",
    "\n",
    "// Determines, whether scipy is defined in ulab. The sub-modules and functions\n",
    "// of scipy have to be defined separately\n",
    "#define ULAB_HAS_SCIPY                      (1)\n",
    "\n",
    "// The maximum number of dimensions the firmware should be able to support\n",
    "// Possible values lie between 1, and 4, inclusive\n",
    "#define ULAB_MAX_DIMS                       2\n",
    "\n",
    "// By setting this constant to 1, iteration over array dimensions will be implemented\n",
    "// as a function (ndarray_rewind_array), instead of writing out the loops in macros\n",
    "// This reduces firmware size at the expense of speed\n",
    "#define ULAB_HAS_FUNCTION_ITERATOR          (0)\n",
    "\n",
    "// If NDARRAY_IS_ITERABLE is 1, the ndarray object defines its own iterator function\n",
    "// This option saves approx. 250 bytes of flash space\n",
    "#define NDARRAY_IS_ITERABLE                 (1)\n",
    "\n",
    "// Slicing can be switched off by setting this variable to 0\n",
    "#define NDARRAY_IS_SLICEABLE                (1)\n",
    "\n",
    "// The default threshold for pretty printing. These variables can be overwritten\n",
    "// at run-time via the set_printoptions() function\n",
    "#define ULAB_HAS_PRINTOPTIONS               (1)\n",
    "#define NDARRAY_PRINT_THRESHOLD             10\n",
    "#define NDARRAY_PRINT_EDGEITEMS             3\n",
    "\n",
    "// determines, whether the dtype is an object, or simply a character\n",
    "// the object implementation is numpythonic, but requires more space\n",
    "#define ULAB_HAS_DTYPE_OBJECT               (0)\n",
    "\n",
    "// the ndarray binary operators\n",
    "#define NDARRAY_HAS_BINARY_OPS              (1)\n",
    "\n",
    "// Firmware size can be reduced at the expense of speed by using function\n",
    "// pointers in iterations. For each operator, he function pointer saves around\n",
    "// 2 kB in the two-dimensional case, and around 4 kB in the four-dimensional case.\n",
    "\n",
    "#define NDARRAY_BINARY_USES_FUN_POINTER     (0)\n",
    "\n",
    "#define NDARRAY_HAS_BINARY_OP_ADD           (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_EQUAL         (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_LESS          (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_LESS_EQUAL    (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_MORE          (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_MORE_EQUAL    (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_MULTIPLY      (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_NOT_EQUAL     (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_POWER         (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_SUBTRACT      (1)\n",
    "#define NDARRAY_HAS_BINARY_OP_TRUE_DIVIDE   (1)\n",
    "...     \n",
    "```\n",
    "\n",
    "The meaning of flags with names `_HAS_` should be obvious, so we will just explain the other options. \n",
    "\n",
    "To see how much you can gain by un-setting the functions that you do not need, here are some pointers. In four dimensions, including all functions adds around 120 kB to the `micropython` firmware. On the other hand, if you are interested in Fourier transforms only, and strip everything else, you get away with less than 5 kB extra. \n",
    "\n",
    "## Compatibility with numpy\n",
    "\n",
    "The functions implemented in `ulab` are organised in four sub-modules at the C level, namely, `numpy`, `scipy`, `utils`, and `user`. This modularity is elevated to `python`, meaning that in order to use functions that are part of `numpy`, you have to import `numpy` as\n",
    "\n",
    "```python\n",
    "from ulab import numpy as np\n",
    "\n",
    "x = np.array([4, 5, 6])\n",
    "p = np.array([1, 2, 3])\n",
    "np.polyval(p, x)\n",
    "```\n",
    "\n",
    "There are a couple of exceptions to this rule, namely `fft`, and `linalg`, which are sub-modules even in `numpy`, thus you have to write them out as \n",
    "\n",
    "```python\n",
    "from ulab import numpy as np\n",
    "\n",
    "A = np.array([1, 2, 3, 4]).reshape()\n",
    "np.linalg.trace(A)\n",
    "```\n",
    "\n",
    "Some of the functions in `ulab` are re-implementations of `scipy` functions, and they are to be imported as \n",
    "\n",
    "```python\n",
    "from ulab import numpy as np\n",
    "from ulab import scipy as spy\n",
    "\n",
    "\n",
    "x = np.array([1, 2, 3])\n",
    "spy.special.erf(x)\n",
    "```\n",
    "\n",
    "`numpy`-compatibility has an enormous benefit : namely, by `try`ing to `import`, we can guarantee that the same, unmodified code runs in `CPython`, as in `micropython`. The following snippet is platform-independent, thus, the `python` code can be tested and debugged on a computer before loading it onto the microcontroller.\n",
    "\n",
    "```python\n",
    "\n",
    "try:\n",
    "    from ulab import numpy as np\n",
    "    from ulab import scipy as spy\n",
    "except ImportError:\n",
    "    import numpy as np\n",
    "    import scipy as spy\n",
    "    \n",
    "x = np.array([1, 2, 3])\n",
    "spy.special.erf(x)    \n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The impact of dimensionality\n",
    "\n",
    "### Reducing the number of dimensions\n",
    "\n",
    "`ulab` supports tensors of rank four, but this is expensive in terms of flash: with all available functions and options, the library adds around 100 kB to the firmware. However, if such high dimensions are not required, significant reductions in size can be gotten by changing the value of \n",
    "\n",
    "```c\n",
    "#define ULAB_MAX_DIMS                   2\n",
    "```\n",
    "\n",
    "Two dimensions cost a bit more than half of four, while you can get away with around 20 kB of flash in one dimension, because all those functions that don't make sense (e.g., matrix inversion, eigenvalues etc.) are automatically stripped from the firmware.\n",
    "\n",
    "### Using the function iterator\n",
    "\n",
    "In higher dimensions, the firmware size increases, because each dimension (axis) adds another level of nested loops. An example of this is the macro of the binary operator in three dimensions\n",
    "\n",
    "```c\n",
    "#define BINARY_LOOP(results, type_out, type_left, type_right, larray, lstrides, rarray, rstrides, OPERATOR)\n",
    "    type_out *array = (type_out *)results->array;\n",
    "    size_t j = 0;\n",
    "    do {\n",
    "        size_t k = 0;\n",
    "        do {\n",
    "            size_t l = 0;\n",
    "            do {\n",
    "                *array++ = *((type_left *)(larray)) OPERATOR *((type_right *)(rarray));\n",
    "                (larray) += (lstrides)[ULAB_MAX_DIMS - 1];\n",
    "                (rarray) += (rstrides)[ULAB_MAX_DIMS - 1];\n",
    "                l++;\n",
    "            } while(l < (results)->shape[ULAB_MAX_DIMS - 1]);\n",
    "            (larray) -= (lstrides)[ULAB_MAX_DIMS - 1] * (results)->shape[ULAB_MAX_DIMS-1];\n",
    "            (larray) += (lstrides)[ULAB_MAX_DIMS - 2];\n",
    "            (rarray) -= (rstrides)[ULAB_MAX_DIMS - 1] * (results)->shape[ULAB_MAX_DIMS-1];\n",
    "            (rarray) += (rstrides)[ULAB_MAX_DIMS - 2];\n",
    "            k++;\n",
    "        } while(k < (results)->shape[ULAB_MAX_DIMS - 2]);\n",
    "        (larray) -= (lstrides)[ULAB_MAX_DIMS - 2] * results->shape[ULAB_MAX_DIMS-2];\n",
    "        (larray) += (lstrides)[ULAB_MAX_DIMS - 3];\n",
    "        (rarray) -= (rstrides)[ULAB_MAX_DIMS - 2] * results->shape[ULAB_MAX_DIMS-2];\n",
    "        (rarray) += (rstrides)[ULAB_MAX_DIMS - 3];\n",
    "        j++;\n",
    "    } while(j < (results)->shape[ULAB_MAX_DIMS - 3]);\n",
    "```\n",
    "\n",
    "In order to reduce firmware size, it *might* make sense in higher dimensions to make use of the function iterator by setting the \n",
    "\n",
    "```c\n",
    "#define ULAB_HAS_FUNCTION_ITERATOR      (1)\n",
    "```\n",
    "\n",
    "constant to 1. This allows the compiler to call the `ndarray_rewind_array` function, so that it doesn't have to unwrap the loops for `k`, and `j`. Instead of the macro above, we now have \n",
    "\n",
    "```c\n",
    "#define BINARY_LOOP(results, type_out, type_left, type_right, larray, lstrides, rarray, rstrides, OPERATOR)\n",
    "    type_out *array = (type_out *)(results)->array;\n",
    "    size_t *lcoords = ndarray_new_coords((results)->ndim);\n",
    "    size_t *rcoords = ndarray_new_coords((results)->ndim);\n",
    "    for(size_t i=0; i < (results)->len/(results)->shape[ULAB_MAX_DIMS -1]; i++) {\n",
    "        size_t l = 0;\n",
    "        do {\n",
    "            *array++ = *((type_left *)(larray)) OPERATOR *((type_right *)(rarray));\n",
    "            (larray) += (lstrides)[ULAB_MAX_DIMS - 1];\n",
    "            (rarray) += (rstrides)[ULAB_MAX_DIMS - 1];\n",
    "            l++;\n",
    "        } while(l < (results)->shape[ULAB_MAX_DIMS - 1]);\n",
    "        ndarray_rewind_array((results)->ndim, larray, (results)->shape, lstrides, lcoords);\n",
    "        ndarray_rewind_array((results)->ndim, rarray, (results)->shape, rstrides, rcoords);\n",
    "    } while(0)\n",
    "```\n",
    "\n",
    "Since the `ndarray_rewind_array` function is implemented only once, a lot of space can be saved. Obviously,  function calls cost time, thus such trade-offs must be evaluated for each application. The gain also depends on which functions and features you include. Operators and functions that involve two arrays are expensive, because at the C level, the number of cases that must be handled scales with the squares of the number of data types. As an example, the innocent-looking expression\n",
    "\n",
    "```python\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3])\n",
    "b = np.array([4, 5, 6])\n",
    "\n",
    "c = a + b\n",
    "```\n",
    "requires 25 loops in C, because the `dtypes` of both `a`, and `b` can assume 5 different values, and the addition has to be resolved for all possible cases. Hint: each binary operator costs between 3 and 4 kB in two dimensions."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The ulab version string\n",
    "\n",
    "As is customary with `python` packages, information on the package version can be found be querying the `__version__` string. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T06:25:27.328061Z",
     "start_time": "2021-01-12T06:25:27.308199Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "you are running ulab version 2.1.0-2D\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "import ulab\n",
    "\n",
    "print('you are running ulab version', ulab.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The first three numbers indicate the major, minor, and sub-minor versions of `ulab` (defined by the `ULAB_VERSION` constant in [ulab.c](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.c)). We usually change the minor version, whenever a new function is added to the code, and the sub-minor version will be incremented, if a bug fix is implemented. \n",
    "\n",
    "`2D` tells us that the particular firmware supports tensors of rank 2 (defined by `ULAB_MAX_DIMS` in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h)). \n",
    "\n",
    "If you find a bug, please, include the version string in your report!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Should you need the numerical value of `ULAB_MAX_DIMS`, you can get it from the version string in the following way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:00:00.616473Z",
     "start_time": "2021-01-13T06:00:00.602787Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "version string:  2.1.0-2D\n",
      "version dimensions:  2D\n",
      "numerical value of dimensions:  2\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "import ulab\n",
    "\n",
    "version = ulab.__version__\n",
    "version_dims = version.split('-')[1]\n",
    "version_num = int(version_dims.replace('D', ''))\n",
    "\n",
    "print('version string: ', version)\n",
    "print('version dimensions: ', version_dims)\n",
    "print('numerical value of dimensions: ', version_num)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ulab with complex arrays\n",
    "\n",
    "If the firmware supports complex arrays, `-c` is appended to the version string as can be seen below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2022-01-07T18:21:04.079894Z",
     "start_time": "2022-01-07T18:21:04.058855Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "version string:  4.0.0-2D-c\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "import ulab\n",
    "\n",
    "version = ulab.__version__\n",
    "\n",
    "print('version string: ', version)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Finding out what your firmware supports\n",
    "\n",
    "`ulab` implements a number of array operators and functions, but this does not mean that all of these functions and methods are actually compiled into the firmware. You can fine-tune your firmware by setting/unsetting any of the `_HAS_` constants in [ulab.h](https://github.com/v923z/micropython-ulab/blob/master/code/ulab.h). \n",
    "\n",
    "### Functions included  in the firmware\n",
    "\n",
    "The version string will not tell you everything about your firmware, because the supported functions and sub-modules can still arbitrarily be included or excluded. One way of finding out what is compiled into the firmware is calling `dir` with `ulab` as its argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-08T12:47:37.963507Z",
     "start_time": "2021-01-08T12:47:37.936641Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "===== constants, functions, and modules of numpy =====\n",
      "\n",
      " ['__class__', '__name__', 'bool', 'sort', 'sum', 'acos', 'acosh', 'arange', 'arctan2', 'argmax', 'argmin', 'argsort', 'around', 'array', 'asin', 'asinh', 'atan', 'atanh', 'ceil', 'clip', 'concatenate', 'convolve', 'cos', 'cosh', 'cross', 'degrees', 'diag', 'diff', 'e', 'equal', 'exp', 'expm1', 'eye', 'fft', 'flip', 'float', 'floor', 'frombuffer', 'full', 'get_printoptions', 'inf', 'int16', 'int8', 'interp', 'linalg', 'linspace', 'log', 'log10', 'log2', 'logspace', 'max', 'maximum', 'mean', 'median', 'min', 'minimum', 'nan', 'ndinfo', 'not_equal', 'ones', 'pi', 'polyfit', 'polyval', 'radians', 'roll', 'set_printoptions', 'sin', 'sinh', 'sqrt', 'std', 'tan', 'tanh', 'trapz', 'uint16', 'uint8', 'vectorize', 'zeros']\n",
      "\n",
      "functions included in the fft module:\n",
      " ['__class__', '__name__', 'fft', 'ifft']\n",
      "\n",
      "functions included in the linalg module:\n",
      " ['__class__', '__name__', 'cholesky', 'det', 'dot', 'eig', 'inv', 'norm', 'trace']\n",
      "\n",
      "\n",
      "===== modules of scipy =====\n",
      "\n",
      " ['__class__', '__name__', 'optimize', 'signal', 'special']\n",
      "\n",
      "functions included in the optimize module:\n",
      " ['__class__', '__name__', 'bisect', 'fmin', 'newton']\n",
      "\n",
      "functions included in the signal module:\n",
      " ['__class__', '__name__', 'sosfilt', 'spectrogram']\n",
      "\n",
      "functions included in the special module:\n",
      " ['__class__', '__name__', 'erf', 'erfc', 'gamma', 'gammaln']\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "from ulab import scipy as spy\n",
    "\n",
    "\n",
    "print('===== constants, functions, and modules of numpy =====\\n\\n', dir(np))\n",
    "\n",
    "# since fft and linalg are sub-modules, print them separately\n",
    "print('\\nfunctions included in the fft module:\\n', dir(np.fft))\n",
    "print('\\nfunctions included in the linalg module:\\n', dir(np.linalg))\n",
    "\n",
    "print('\\n\\n===== modules of scipy =====\\n\\n', dir(spy))\n",
    "print('\\nfunctions included in the optimize module:\\n', dir(spy.optimize))\n",
    "print('\\nfunctions included in the signal module:\\n', dir(spy.signal))\n",
    "print('\\nfunctions included in the special module:\\n', dir(spy.special))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Methods included in the firmware\n",
    "\n",
    "The `dir` function applied to the module or its sub-modules gives information on what the module and sub-modules include, but is not enough to find out which methods the `ndarray` class supports. We can list the methods by calling `dir` with the `array` object itself:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-08T12:48:17.927709Z",
     "start_time": "2021-01-08T12:48:17.903132Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['__class__', '__name__', 'copy', 'sort', '__bases__', '__dict__', 'dtype', 'flatten', 'itemsize', 'reshape', 'shape', 'size', 'strides', 'tobytes', 'transpose']\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "print(dir(np.array))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Operators included in the firmware\n",
    "\n",
    "A list of operators cannot be generated as shown above. If you really need to find out, whether, e.g., the `**` operator is supported by the firmware, you have to `try` it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-08T12:49:59.902054Z",
     "start_time": "2021-01-08T12:49:59.875760Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "operator is not supported:  unsupported types for __pow__: 'ndarray', 'ndarray'\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3])\n",
    "b = np.array([4, 5, 6])\n",
    "\n",
    "try:\n",
    "    print(a ** b)\n",
    "except Exception as e:\n",
    "    print('operator is not supported: ', e)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The exception above would be raised, if the firmware was compiled with the \n",
    "\n",
    "```c\n",
    "#define NDARRAY_HAS_BINARY_OP_POWER         (0)\n",
    "```\n",
    "\n",
    "definition."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "382.797px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}