Numpy sous micropython !

La réflexion

Les tableaux numpy sont un "must" du langage Python pour manipuler des valeurs numériques. C'est de facto une sorte de "standard" en CPython. Pour dire les choses simplement, un tableau Numpy, çà reprend les concepts de l'objet list mais en l'étendant à des tableaux multidimensionnels et en permettant des opérations sur tous les éléments simultanément à l'aide d'opérations ou fonctions comme on le ferait sur des variables. Bref, c'est un "must".

Techniquement, Numpy réalise des opérations sur des tableaux de valeurs numériques, ce que l'on appelle des vecteurs, comme il le ferait sur des variables : tous les éléments du tableau de valeur sont concernés par l'opération appliquée au tableau.

Par contre, la librairie Numpy CPython est beaucoup trop volumineuse pour être disponible en Micropython en intégralité. Mais en même temps, le besoin de dont il faudrait pouvoir disposer sur microcontrôleur peut facilement être limité.

On va explorer ici les possibilités en Micropython.

Pourquoi faire çà ?

Comme le dit la doc du numpy ulab (voir ci-après) https://micropython-ulab.readthedocs.io/en/latest/ulab-intro.html, "Of course, the first question that one has to answer is, why on Earth one would need a fast math library on a microcontroller. "... "Bien sûr, la première question à laquelle il faut répondre est de savoir pourquoi on a besoin d'une bibliothèque mathématique rapide sur un microcontrôleur."

Pourquoi on voudrait faire çà ? à priori, çà va "blinder" des ressources du microcontrôleur... à priori... Mais en fait, il y a plusieurs raisons potentiellement valables :

  • pour disposer d'un traitement rapide de données, avec des fonctions avancées. AInsi, le créateur de numpy-ulab avait besoin d'une transformée de Fourier (extraction des fréquences) sur un signal mesuré par une borche analogique de la carte utilisée (la pyboard en l'occurrence). Le tout de façon assez rapide, d'où la nécessité de compiler nativement la librairie...

  • parce que "numpy", c'est un peu un "Saint Graal" quand on code en Python : çà sert tout le temps... et pour pleins de domaines utilisant des tableaux de valeurs. Probablement pas indispensables voire même utile sur une carte à micro-contrôleur, mais çà permet d'apprendre, de se familiariser avec un tableau numpy... qui seront disponibles en CPython. Donc dans une approche didactique, c'est très intéressant.

  • parce que si on peut écrire des codes utilisant numpy sur microcontrôleur... et bien çà veut dire qu'on va pouvoir réutiliser des codes qui fonctionnent en CPython "as is" sur le microcontrôleur et inversement. Dans la série "je potentialise les codes que j'écris", ben c'est pas mal... ! C'est probablement un des arguments les plus pertinents

  • parce que Micropython ne cesse pas de nous surprendre. A la base, c'est presque finalement une expérimentation : qu'est-ce que çà donnerait un Python sur microcontrôleur... Beaucoup n'aurait pas miser grand chose là-dessus au départ... mais quand on voit où on est actuellement, et bien on se dit qu'on peut bien continuer l'expérimentation... Et si on pouvait avoir numpy, çà donnerait quoi ? est-ce possible ? Juste pour voir...

  • et parce que les plateformes à microcontrôleur vont être de plus en plus puissante avec le temps. On a déjà des STM à 600Mhz... donc même si actuellement c'est peut-être encore un peu "limite"... les choses vont probablement s'améliorer. ...

  • just for fun ! Au moins pour voir ce qu'il est possible, qui à laisser de côté si on en n'a pas besoin, mais au moins voir ce que çà donne. Les "esprits pythoniques" comprendront...

  • et parce que si on a Numpy sur un micro-contrôleur... et bien çà va faire "lever les oreilles" de tous ceux qui codent déjà en Python... "Ah ouais, tu fais çà sur une carte à 4€... ah ouais, quand même... c'est dingue ce truc... !" suivi d'un innocent "Et c'est quoi la manip' pour tester ?... juste pour voir... "

un numpy sous micropython existe !

Le projet ulab propose une version de micropython intégrant un module numpy-like :

Cette solution nécessite un micropython compilé avec numpy. La doc est complète.

Clé en main

On trouvera des version de Micropython incluant numpy de ulab ici pour la plupart des plateformes (ESP 32, Pyboard, et même le Pi Pico ! ) :

A noter que le fichier binaire faire 800Ko, contre 500Ko environ pour Micropython seul.

Si on veut le faire soi-même

Si on veut le compiler soi-même, on le trouvera ici : https://github.com/v923z/micropython-ulab#compiling

Et aussi - état des lieux des possibilités

On peut implémenter la gaussienne en pure python facilement : * https://introcs.cs.princeton.edu/python/21function/gauss.py.html

  • une librairie de math sur vecteurs (= liste de valeurs) en Python : très simple et finalement suffisant pour des choses de base : https://gitlab.com/nickoala/micropython-vec Passe par des fonctions dédiées. Le code est intéressant et facile.

  • https://github.com/billtubbs/array_funcs : des fonctions écrite en inline assembler pour micropython et qui permettent d'obtenir un comportement ndarray like mais il faut appeler les fonctions.

Petit test

Simplement flasher sa carte Pi Pico avec la version pimoroni de micropython (la 1.15 à l'heure de mon test) :

  • carte débranchée, appui sur "BOOTSEL",
  • brancher la carte
  • copier le UF2 voulu

Une fois fait, ouvrir Thonny ou autre et dans l'interpréteur, on peut tester :

>>>from ulab import numpy as np
>>>np.ones(100)
>>>x=np.arange(100)
>>>print(x)
array([0, 1, 2, ..., 97, 98, 99], dtype=int16)
>>>y=np.sin(x)
>>>print(y)
array([0.0, 0.8414709, 0.9092974, ..., 0.3796077, -0.5733819, -0.9992067], dtype=float32)

Bah... bah... bah... ! C'est tout simplement énorme : çà fonctionne ! J'ai limpression d'être dans un notebook ou l'interpréteur Jupyter... mais non, je suis sur le microcontrôleur !

Là je suis sur le Pi Pico (à moins de 5€) et j'ai juste flashé avec la version pimoroni... Bref, simple !

Note

J'en reste là pour le petit test de débrouillage, mais clairement, la doc de la lib' est ENORME, quasiment un remake de celle de numpy en CPython (qui est une très très grosse doc). Mais le truc le plus incroyable, c'est qu'on dispose non seulement des tableaux Numpy... mais aussi de fonctions qu'on a dans Scipy, telle que la tranformée de Fourier, etc... Probablement, on a un nombre limité de fonctions Scipy, cette librairie étant énorme en CPython, mais quand même, on a quelques-unes qui peuvent s'avérer très intéressantes

La suite

La première chose que l'on a envie de faire, c'est de "timer" un peu les exécutions de fonctions, sur une taille de tableau "standard", disons 128 valeurs, ce qui est la largeur d'un petit écran OLED par exemple.

Avec TinyNUmpy

TinyNumpy est une version allégée de micropython, en pure Python d'où l'idée de tester çà.

Pour l'avoir fait :

"Mini ou micro-numpy" avec des list ou classe implémentée ?

Error

Pour avoir tenté l'affaire, les choses ne sont pas si simples et l'utilisation de ulab numpy est la bonne option si on a besoin de numpy en micropython.

On peut aussi tout simplement partir sur des list et se faire quelques fonctions qui donne un comportement "numpy-like"... mais çà ne vaut pas le numpy. Ceci étant, on peut essayer malgré tout d'obtenir une syntaxe équivalente à celle de numpy qui permettrait au moins de pouvoir utiliser des codes micropython à l'identique dans un matplotlib par exemple. Ou pour le dire autrement, on pourrait initier à la syntaxe Numpy dès l'apprentissage du code micropython. C'est probablement çà un des intérêts majeurs.

Un premier objectif, par exemple, serait de pouvoir utiliser des arange pour grapher des courbes sur écran OLED, etc, comme on le ferait pour un graphique matplotlib dans Jupyter.

Quelques débrouillages :

def arange(start=0,end=1, step=0.1):

    out=[i*step for i in range(start,int(end/step))]
    return out

>>> arange(0,1,0.1)
[0.0,
 0.1,
 0.2,
 0.30000000000000004,
 0.4,
 0.5,
 0.6000000000000001,
 0.7000000000000001,
 0.8,
 0.9]

La question qui se pose alors, c'est comment implémenter les opérateurs pour une classe ou même les opérateurs eux-mêmes :

On a 2 options :

  • soit on crée une classe équivalente du ndarray et on implémente les opérations sur cette classe. cela semble plus pertinent.
  • soit on reste sur des list et on ré-implémente les opérateurs pour des list. çà semble moins pertinent.

Un point de départ très intéressant serait de s'inspirer, en simplifiant fortement les types possibles, etc, de ce qui est fait ici pour implémenter la classe ndarray : https://github.com/wadetb/tinynumpy/blob/master/tinynumpy/tinynumpy.py

Un premier objectif serait de pouvoir facilement créer les objets fondamentaux de numpy et réaliser des opérations dessus :

>>> import micronumpy as np

>>> x=np.arange(0,1,0.2)

>>> x
Out[7]: array([0. , 0.2, 0.4, 0.6, 0.8])

>>> x*3
Out[8]: array([0. , 0.6, 1.2, 1.8, 2.4])

>>> x/2
Out[9]: array([0. , 0.1, 0.2, 0.3, 0.4])

>>> np.sin(x)
Out[10]: array([0.        , 0.19866933, 0.38941834, 0.56464247, 0.71735609])

Et un second objectif serait par exemple de pouvoir écrire en micropython un code du type (affichage d'une gaussienne) :

import micronumpy as np
import microscipy as scipy
from graph2D import plot

sigma=0 # centrage
mu=1 # largeur

x=np.linspace(-4,4,100) # 100 valeurs entre min et max
y1=scipy.stats.norm(sigma,mu).pdf(x)

plot(x,y1) 

Note

Par contre, la question qui se pose c'est quel intérêt, puisqu'on dispose de micropython compilés avec ulab numpy ? Une bonne raison de le faire pourrait notamment être le fait que ulab numpy n'implémente pas certaines fonctions dont on aimerait pouvoir disposer. Je pense par exemple à la courbe gauss, etc. Et il faudrait pouvoir le faire en Micropython, sans avoir à recompiler. Les performances seraient probablement moins bonnes, mais çà permettrait d'écrire du code "numpy-like" en micropython et l'étendre facilement selon ses besoins.

Après, on peut déjà simplement explorer la faisabilité et si ce n'est pas trop complexe, poursuivre en fonction des besoins. Déjà une classe ndarray avec la gestion de l'addition , de la multiplication, etc. applicable au tableau, çà serait pas mal du tout.

Implémentations existantes ?

Il y a déjà quelques tentatives en ce sens, notamment :