"Numpy-like" en "Built-in"
Intro
Lorsque l'on vient de CPython, on a l'habitude d'utiliser des séries de valeur, typiquement une série de valeurs x et y en vue d'un affichage de courbes x,y, ou encore pour des courbes paramétriques où à partir d'une série t, on a une courbe x(t), y(t).
La librairie que l'on utilise dans ce type de situation est Numpy. Elle permet la manipulation facile de séries de valeurs, y compris de grande dimension, ainsi que de nombreuses fonctions mathématiques. Elle permet également la manipulation de tableaux multidimensionnels, etc.
Sur microcontrôleur, on a une double contrainte :
-
d'une part la librairie Numpy est beaucoup trop grosse pour être implémentée en intégralité, et même si il y a d'ors et déjà une implémentation Numpy sous Micropython, elle n'en reste pas moins gourmande en ressources,
-
d'autre part, le besoin sur microcontrôleur est somme toute limité, à savoir essentiellement gérer des séries de valeurs 1D voire 2D, mais guère plus.
En fait, en y réfléchissant un peu, dans un contexte de microcontrôleur aux ressources limitées, les classes et fonctions builtins reprennent tout leur sens, et on se rend compte au passage, qu'en CPython on peut malgré tout prendre de mauvaises habitudes, à savoir utiliser un peu systématiquement "l'artillerie lourde" là où on pourrait le faire assez simplement avec le langage (Micro)Python lui-même. Finalement, pour celui qui est habitué à coder en CPython, le fait de coder en MicroPython sur microcontrôleur impose de revenir au langage Python lui-même, ce qui est finalement salutaire d'un certain point de vue car c'est paradoxalement une attitude que l'on a tendance à perdre en CPython.
Petite démonstration / exploration du sujet ici, en Micropython : on va (re)-découvrir ici tout le potentiel de la comprehension list
.
Note
Un point positif de cette approche est, il me semble, que l'on reste sur la syntaxe de base de Python sans avoir à appréhender la syntaxe d'une nouvelle librairie, ce qui est souvent source de frustration au début de la prise en main d'une librairie telle que numpy. (Re)-découvrir que l'on peut faire les choses simplement est intéressant... justement pour les situations simples.
Les vecteurs de base
Les vecteurs est l'autre nom des "série de valeur", à ne pas confondre avec le vecteur de la géométrie ou de la physique qui est le concept d'une grandeur associé à une direction. Ici, on ne parle que de série de valeur.
Le ones
Avec numpy, il est facile de créer une série de valeurs valant 1 avec le ones
. Voici comment faire le ones
facilement :
x=[1]*10 # Un list de 10 valeurs 1
print(x)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
On obtiendrait exactement le même résultat en faisant :
x=[1 for i in range(10)] # Un list de 10 valeurs 1
print(x)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Le zeros
De la même façon, avec numpy, il est facile de créer une série de valeurs valant 0 avec le zeros
. Voici comment faire le zeros
facilement :
x=[0]*10 # Un list de 10 valeurs 1
print(x)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Et comme pour le ones
, on obtiendrait exactement le même résultat en faisant :
x=[0 for i in range(10)] # Un list de 10 valeurs 1
print(x)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Le arange
Avec Numpy, il est possible de créer un vecteur de valeurs bien plus utile que le ones
ou zeros
, à savoir le arange
est une plage de valeurs décimales définies par une valeur de début, de fin et un pas. C'est comme le range
mais avec les nombres à virgules, ce que ne supporte pas le range
. Il est cependant assez facile d'obtenir un équivalent du arange
sous forme d'un list
en posant :
x=[ i*0.1 for i in range(0, 10)] # un arange
print(x)
0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
On peut généraliser un peu les choses en créant un petite fonction dédiée :
def arange(start, end, step):
return [start+(el*step) for el in range(0,int((end-start)/step))]
x=arange(0,10,0.1)
print(x)
[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 7.0, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8.0, 8.1, 8.2, 8.3, 8.400001, 8.5, 8.6, 8.7, 8.8, 8.900001, 9.0, 9.1, 9.2, 9.3, 9.400001, 9.5, 9.6, 9.7, 9.8, 9.900001]
Le linspace
Avec Numpy, le linspace
permet d'obtenir n valeurs régulièrement espacées dans une plage donnée (ici, on précise le nombre de valeurs au lieu du step comme on le fait avec la arange). Assez logiquement, le pas du linspace
inclus la valeur finale. On l'obtient en posant :
``````python def linspace(start, end, num): step=(end-start)/(num-1) return [start+(el*step) for el in range(0,num)]
x=linspace(0,10,30) print(x) [0.0, 0.3448276, 0.6896552, 1.034483, 1.37931, 1.724138, 2.068965, 2.413793, 2.758621, 3.103448, 3.448276, 3.793103, 4.137931, 4.482759, 4.827586, 5.172414, 5.517241, 5.862069, 6.206897, 6.551724, 6.896552, 7.241379, 7.586207, 7.931035, 8.275862, 8.620689, 8.965517, 9.310345, 9.655172, 10.0]
x=linspace(5,10,15) print(x) [5.0, 5.357143, 5.714286, 6.071428, 6.428572, 6.785714, 7.142857, 7.5, 7.857143, 8.214286, 8.571428, 8.928572, 9.285714, 9.642857, 10.0] ```
Note
A titre de comparaison, voici les sorties de la fonctions numpy équivalente en CPython :
``` import numpy as np
np.linspace(0,10,30) Out[19]: array([ 0. , 0.34482759, 0.68965517, 1.03448276, 1.37931034, 1.72413793, 2.06896552, 2.4137931 , 2.75862069, 3.10344828, 3.44827586, 3.79310345, 4.13793103, 4.48275862, 4.82758621, 5.17241379, 5.51724138, 5.86206897, 6.20689655, 6.55172414, 6.89655172, 7.24137931, 7.5862069 , 7.93103448, 8.27586207, 8.62068966, 8.96551724, 9.31034483, 9.65517241, 10. ])
np.linspace(5,10,15) Out[18]: array([ 5. , 5.35714286, 5.71428571, 6.07142857, 6.42857143, 6.78571429, 7.14285714, 7.5 , 7.85714286, 8.21428571, 8.57142857, 8.92857143, 9.28571429, 9.64285714, 10. ]) ```
Comme on peut le voir, à quelques décimales près ( à partir de la 7ème après la virgule...!), le résultat est le même. MicroPython s'en sort très bien.
Un vecteur de valeurs randomisées
Avec Numpy, le empty
est un vecteur de valeurs randomisées comprises entre 0 et 1. On peut obtenir la même chose facilement en faisant :
python
import random
x=[random.random() for i in range(10)] # série de n valeur aléatoire = le empty
print(x)
[0.6827134, 0.2670938, 0.4636654, 0.9186064, 0.767941, 0.6937876, 0.7738847, 0.517539, 0.5688641, 0.1101506]
On peut également envisager une série de n valeurs aléatoires comprises dans une plage donnée à l'aide de la fonction randrange
ce qui donne par exemple :
python
import random
x=[random.randrange(0,100) for i in range(10)] # série de n valeur aléatoires
print(x)
[74, 50, 14, 22, 46, 74, 85, 41, 78, 9]
Operations sur les éléments d'un vecteur
Une des grandes force de Numpy est de permettre d'appliquer des fonctions sur l'ensemble des valeurs du vecteur, tout comme on le ferait sur une variable. A partir du moment où nous utilisons des list
, cela ne sera pas directement possible, mais un résultat tout à fait comparable est obtenu plutôt simplement en "pure-(Micro)Python".
Avec une comprehension list
Première solution, assez simple, c'est d'utiliser la comprehension list qui est vraiment un "couteau suisse" :
```python import math
x=range(0,360,36) y=[math.sin(math.radians(el)) for el in x] # fonction appliquée sur le vecteur de valeurs y [0.0, 0.5877852, 0.9510566, 0.9510566, 0.5877852, -5.7742e-08, -0.5877851, -0.9510565, -0.9510565, -0.5877853] ```
Avec la fonction built-in map
Une autre possibilité est d'utiliser la fonction built-in map()
qui permet d'appliquer une fonction à tous les éléments d'un objet séquence, et donc d'un list
Attention, map
renvoie un itérateur, à caster en list
pour accéder aux valeurs.
Première possibilité, en utilisant une fonction inline lambda
:
```python
x=list(range(0,100, 10)) y=list(map(lambda x: x**2, x)) print(y) [0, 100, 400, 900, 1600, 2500, 3600, 4900, 6400, 8100] ```
Et l'autre possibilité, encore plus souple potentiellement dans ses possiblités, est d'appeler une fonction dédiée :
```python import math
def f(x): out=math.sin(math.radians(x)) return out
x=range(0,360,36) y=list(map(f,x)) print(y)
[0.0, 0.5877852, 0.9510566, 0.9510566, 0.5877852, -5.7742e-08, -0.5877851, -0.9510565, -0.9510565, -0.5877853] ```
C'est moins compact, mais potentiellement sans limite de possibilités !
Opérations itérative sur les éléments d'un vecteur
Nous avons vu précédemment comment effectuer facilement des opérations élément par élément sur un vecteur de valeurs. Nous allons voir ici comment appliquer une opération récursive sur un vecteur qui renvoie un vecteur dont chaque élément est la résultante d'une opération sur l'ensemble des éléments précédents du vecteur d'origine.
Typiquement, un exemple de çà est le calcul de "l'intégrale" d'un vecteur de valeurs :
```python x=list(range(10))
y=[sum(x[:idx])+el for idx,el in enumerate(x)] print(y) [0, 1, 3, 6, 10, 15, 21, 28, 36, 45] ```
C'est l'équivalent du cumsum()
en numpy.
On peut tester çà par exemple par l'intégration d'une fonction linéaire qui abouti à une fonction puissance 2 (parabole) :
```python t=list(range(10)) y1=[2el for el in t] # 2t
y2=[sum(y1[:idx])+el for idx,el in enumerate(y1)] # cumsum print(y2) [0, 2, 6, 12, 20, 30, 42, 56, 72, 90]
y3=[(el**2)+el for el in t] # integrale print(y3) [0, 2, 6, 12, 20, 30, 42, 56, 72, 90]
```
Sur la récursion , voir cet excellent post : https://stackoverflow.com/questions/30214531/basics-of-recursion-in-python
Opérations sur l'ensemble des éléments d'un vecteur
Nous allons voir ici comment appliquer une opération qui utilise l'ensemble des éléments d'un vecteur de valeurs.
Somme des éléments
python
import math
x=list(range(0,10))
y=sum(x)
45
Produit des éléments
De la même façon, avec la fonction (non disponible en MicroPython) avec une fonction : prod
du module standard math
(Python 3.8)
Pour mémoire, le produit d'une suite se note "grand Pi" : https://fr.wikipedia.org/wiki/Notation_(math%C3%A9matiques)#Produit
```python import math
def prod(x):
result=1
for i in x:
result=result*i
return result
x=list(range(1,10)) y=prod(x) print(y)
362880 ```
A noter que la factorielle n'est qu'un cas particulier.
Voir : https://computersciencehub.io/python/python-multiplying-all-numbers-in-a-list-with-each-other/
Avec les fonctions built-in
existantes appliquées à l'ensemble d'un tableau
Certaines fonctions built-in existantes peuvent être appliquées sur les objets list
max()
python
import random
x=[random.randrange(0,100) for el in range(10)] # serie de 10 nombres aleatoires
print(x)
[88, 43, 44, 23, 37, 41, 17, 17, 8, 43]
print(max(x))
88
min()
print(max(x))
8
sorted()
print(sorted(x))
[8, 17, 17, 23, 37, 41, 43, 43, 44, 88]
Avec les fonctions de la classe list
Rappel : Avec les fonctions de l'objet list
, l'objet list
lui-même est modifié alors que dans le cas des fonctions précédentes, l'objet list
source n'est pas modifié
```
x.reverse() x [43, 8, 17, 17, 41, 37, 23, 44, 43, 88] x.reverse() x [88, 43, 44, 23, 37, 41, 17, 17, 8, 43] x.sort() x [8, 17, 17, 23, 37, 41, 43, 43, 44, 88] ```
Opérations entre vecteurs
Une des opérations "magique" les plus intéressante que permet numpy concerne les opérations "terme à terme" entre 2 vecteurs de valeurs. En utilisant les possibilités built-in
, les choses ne sont pas si compliquées que çà :
```python
x=range(0,100,10) # 2 list identiques y=range(0,100,10) z=[elx-ely for elx,ely in zip(x,y)] print(z) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
z=[elx-ely for elx,ely in zip(x,y)] # soustraction print(z) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
z=[elx+ely for elx,ely in zip(x,y)] # addition print(z) [0, 20, 40, 60, 80, 100, 120, 140, 160, 180]
z=[elx*ely for elx,ely in zip(x,y)] # multiplication print(z) [0, 100, 400, 900, 1600, 2500, 3600, 4900, 6400, 8100]
z=[elx**ely for elx,ely in zip(x,y)] #puissance print(z) [1, 10000000000, 104857600000000000000000000, 205891132094649000000000000000000000000000000, 12089258196146291747061760000000000000000000000000000000000000000, 8881784197001252323389053344726562500000000000000000000000000000000000000000000000000, 48873677980689257489322752273774603865660850176000000000000000000000000000000000000000000000000000000000000, 1435036016098684342856030763566710717400773837392460666392490000000000000000000000000000000000000000000000000000000000000000000000, 176684706477838432958329750074291851582748389687561895812160620129261977600000000000000000000000000000000000000000000000000000000000000000000000000000000, 76177348045866392339289727720615561750424801402395196724001565744957137343033038019601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000]
```
Remarquer au passage la dimension des entiers supportée par MicroPython.... !
A titre indicatif, la même chose en CPython donne :
[1, 10000000000, 104857600000000000000000000, 205891132094649000000000000000000000000000000, 12089258196146291747061760000000000000000000000000000000000000000, 8881784197001252323389053344726562500000000000000000000000000000000000000000000000000, 48873677980689257489322752273774603865660850176000000000000000000000000000000000000000000000000000000000000, 1435036016098684342856030763566710717400773837392460666392490000000000000000000000000000000000000000000000000000000000000000000000, 176684706477838432958329750074291851582748389687561895812160620129261977600000000000000000000000000000000000000000000000000000000000000000000000000000000, 76177348045866392339289727720615561750424801402395196724001565744957137343033038019601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000]
Le résultat est strictement idem. MicroPython est bluffant ! Et clairement, comparativement à d'autres langages, notamment Arduino, on ne joue pas dans la même catégorie avec MicroPython !
Conclusion
Le langage Python lui-même s'avère plutôt efficace pour réaliser des opérations "à la numpy" sur des vecteurs de valeur de taille limitée, et ces principes seront bien utiles pour l'utilisation d'afficheurs graphiques.