02 mai 2020 — Python
Si vous arrivez ici, vous avez peut-être lu l’article sur la construction des nombres réels.
Avant de vous parler d’un autre ensemble de nombres fondamentaux en mathématiques, les nombres complexes, je voudrais faire un petit détour par les polynômes.
Ceci permettra d’une part de définir précisément les nombres complexes, mais aussi de généraliser le concept sous-jacent d’extension de corps par un polynôme irréductible.
Nous finirons avec une application pratique des polynômes en informatique, en parlant de QR codes et de codes correcteurs.
D’après Wikipedia, Leonardo Fibonacci aurait posé quelque part autour de 1202 une énigme sur l’évolution mois après mois du nombre de couples de lapins en partant d’un unique couple placé dans un enclos.
Cette modélisation très sommaire a donné une suite qui porte son nom, et qui a bien plus d’utilité pour illustrer des notions mathématiques ou algorithmiques que pour réellement compter des lapins 🙂
Le calcul des valeurs de cette suite est un exercice classique de programmation.
Une approche naïve avec une fonction récursive est la suivante :
def fibonacci(n):if n in (0, 1):return 1else:return fibonacci(n-2) + fibonacci(n-1)>>> fibonacci(5)8>>> fibonacci(30)1346269
Cette approche est assez mauvaise, chaque appel à la fonction lançant deux nouveaux appels pour calculer les termes précédents, la complexité algorithmique est alors exponentielle.
On peut trouver des algorithmes qui améliorent ce temps de calcul, je vous laisse par exemple proposer une version qui calcule les différents termes de façon itérative.
En fait, il est possible de donner une formule explicite pour en fonction de .
Une « astuce » consiste à remarquer que si l’on considère une suite géométrique de la forme , la condition de récurrence nous ramène à l’équation polynomiale .
Cette équation s’écrit encore et a donc deux solutions données par et .
En cherchant une solution sous forme d’une combinaison linéaire des et et en choisissant les coefficients permettant de vérifier les conditions initiales requises, on obtient alors la formule explicite :
L’astuce que nous avons utilisée ci-dessus semble un peu « parachutée ». En tout cas, c’est l’impression que j’ai eue la première fois qu’on me l’a présentée.
Elle correspond en fait à un cas particulier de recherche des valeurs propres d’une application linéaire, via l’étude de son polynôme caractéristique (ici le polynôme ).
Cette même astuce est utilisée, souvent de façon implicite, dans de nombreux domaines de la physique lorsque les systèmes étudiés sont modélisés par des équations linéaires.
Une chose remarquable est qu’elle reste utilisable pour trouver des formules explicites même lorsque l’équation polynomiale correspondante n’admet pas de solution réelle, ce qui est à l’origine de l’utilisation des nombres complexes dans beaucoup de problèmes issus du monde réel… nous aurons l’occasion d’y revenir 😉
Les expressions polynômiales sont celles de la forme , où les coefficients sont des nombres réels (pour l’instant, mais nous étendrons ce concept à d’autres ensembles).
Par exemple, nous avons étudié le polynôme pour trouver une expression des termes de la suite de Fibonacci.
Les mathématiciens utilisent plutôt un majuscule pour écrire les polynômes formels, notation que nous adopterons ici.
Considérer un polynôme est équivalent à se donner une suite finie de nombres , étant l’indice du dernier terme non nul s’il existe ( est le degré du polynôme).
L’addition de deux polynômes est définie assez naturellement en additionnant terme à terme.
La multiplication de deux polynômes se fait en étendant les règles usuelles de distributivité de la multiplication :
Plus généralement, si l’on considère le produit du polynôme de coefficients par le polynôme de coefficients , le développement du produit nous donne un polynôme de degré .
Ses coefficients sont définis par
( est la somme des produits avec )
💡 La formule ci-dessus est un cas particulier d’une opération appelée produit de convolution. Le produit de convolution est utilisé dans différents domaines des mathématiques, par exemple pour déterminer la loi d’une somme de variables aléatoires indépendantes…
Tout comme pour les entiers, on définit une notion de divisibilité sur les polynômes :
Un polynôme est divisible par un polynôme si il existe un polynôme tel que .
Voici une implémentation simple d’un type Polynomial
, avec Python 3 : voir le code source.
Nous avons vu qu’un polynôme est défini par une suite finie de coefficients, que nous pouvons modéliser à l’aide d’une liste. J’ai privilégié l’utilisation des tuples, qui sont immuables.
Ce type utilise la définition des opérateurs en Python, en particulier les opérateurs numériques (addition, multiplication, etc.).
Quelques détails d’implémentation :
pow
en utilisant
un algorithme d’exponentiation rapide. Je vous laisse le faire en exercice 😉__radd__
, etc.) pour supporter des opérations comme 1 + Polynomial(0, 1)
.Effectuons quelques calculs :
>>> Polynomial(1, 2, 3) * Polynomial(3, 0, -5)(3, 6, 4, -10, -15)
Ou, si vous préférez l’écriture « avec des » :
>>> X = Polynomial(0, 1)>>> (1 + 2*X) * (2 - 3*X**2)(2, 4, -3, -6)
Une opération moins évidente sur les polynômes est la division euclidienne.
Pour les entiers naturels, la division euclidienne de par consiste à obtenir une écriture de la forme avec . Un algorithme pour obtenir cette décomposition consiste à retrancher à jusqu’à obtenir la condition requise.
Pour des polynômes, la notion de degré permet d’obtenir une décomposition analogue :
et étant des polynômes avec , il existe des polynômes (quotient) et (reste) uniques vérifiant : avec
L’algorithme est un peu plus complexe, puisque cette fois-ci on doit retrancher à chaque étape un polynôme bien choisi de façon à faire diminuer le degré du reste.
Voici l’implémentation de la division euclidiene dans le type Polynomial
:
def __divmod__(self, other):other = Polynomial.from_obj(other)if other is None:return NotImplementedif other.iszero:raise ZeroDivisionErrorQ, R = 0, selfwhile R.degree >= other.degree:ratio = R[R.degree] * other[other.degree]**-1monomial = Polynomial(*([0] * (R.degree - other.degree) + [ratio]))R -= monomial * otherQ += monomialreturn (Q, R)
Prenons un exemple simple :
>>> X = Polynomial(0, 1)>>> divmod(1 + 2*X + X**3, 2 * X**2 - X)((0.25, 0.5), (1.0, 2.25))
Ce qui traduit l’égalité :
Explications du fonctionnement de l’algorithme :
💡 Nous utilisons dans cet algorithme le nombre .
Lorsque nous considérerons des polynômes dont les coefficients sont dans d’autres ensembles que les réels, il faudra
toujours avoir une notion d’inverse pour la multiplication afin que cet algorithme fonctionne : il restera valable
pour des polynômes à coefficients dans des corps.
Si vous avez lu l’article d’introduction à l’arithmétique modulaire et la construction de , vous savez que compter dans cet ensemble revient à compter « à un multiple de près ».
Cette idée s’étend aux ensembles de polynômes. Si l’on particularise un polynôme de degré , on va pouvoir compter « à un polynôme multiple de près », c’est-à-dire modulo .
Quel intérêt à cela ?
Voici quelques exemples de calcul modulo .
>>> P = Polynomial(2, 4, -3, -6)>>> Polynomial(1, 2, 3) * Polynomial(4, 5, 6) % P(10.0, 31.0, 31.0)>>> Polynomial(1, 2) * Polynomial(2, 0, -3) % P0
On constate dans le second exemple que les polynômes et sont des diviseurs de modulo . Ceci s’explique par le fait que .
Si l’on veut pouvoir introduire une division sur les polynômes modulo et ainsi pouvoir y calculer « comme sur les réels », il ne doit pas y avoir de diviseur de zéro.
Cela nécessite qu’on ne puisse pas exprimer comme produit de polynômes non constants (cas précédent), on dit alors que est irréductible. Nous allons voir que cette condition est en fait suffisante.
La question de savoir si un polynôme a un polynôme inverse modulo revient en fait à savoir si il existe des polynômes et tels que . Dans ce cas, on pourra poser .
Étudions ci-dessous les conditions pour que cela soit possible.
Notons l’ensemble des polynômes qui peuvent s’écrire sous la forme , avec et des polynômes quelconques (on peut voir comme une fonction qui à deux polynômes associe un ensemble de polynômes).
On peut construire deux suites finies de polynômes et en posant :
La suite des étant strictement décroissante, on atteint la condition en un nombre fini d’étapes.
Toute la subitlité de cet algorithme consiste à identifier l’invariant suivant :
À la fin du procédé, on a et donc .
Notons le polynôme unitaire (où est le coefficient dominant de ).
est l’ensemble des polynômes multiples de (en particulier et sont donc multiples de ).
Si l’on résume les résultats obtenus :
Il existe un unique polynôme unitaire , appelé le PGCD de et , tel que :
- divise et divise
- peut s’écrire sous la forme (identité de Bézout)
- Tout diviseur commun de et divise .
On déduit le dernier point de l’identité de Bézout, et il justifie le nom de « plus grand ».
💡 Dans le cadre de la théorie des anneaux, est l’idéal engendré par et . L’algorithme nous montre que cet idéal est principal, c’est-à-dire engendré par un unique élément .
Voici une implémentation de cet algorithme en Python :
@classmethoddef bezout_identity(cls, A, B):U, V = 1, 0 # Invariant: A = U*A_n + V*B_nu, v = 0, 1 # Invariant: B = u*A_n + v*B_nwhile not B.iszero:Q, R = divmod(A, B)A, B = B, R# A' = B, B' = A - Q BU, V, u, v = u, v, U - Q * u, V - Q * vmult = A[A.degree]**-1return (mult * U, mult * V, mult * A)
Dans cette implémentation, on calcule aussi les polynômes et de l’identité de Bézout, en maintenant à chaque étape cette décomposition pour et .
>>> Polynomial.bezout_identity(2*X**6 - 2, X**9 - 1)((0.0, 0.0, 0.0, -0.5), 1.0, (-1.0, 0.0, 0.0, 1.0))
On déduit de ce qui précède le critère suivant d’inversibilité, similaire à celui qu’on avait obtenu pour les entiers :
Un polynôme est inversible pour la multiplication modulo si et seulement si (on dit que est premier avec ).
On déduit de ce qui précède :
Tous les polynômes non nuls ont un inverse modulo si et seulement si est irréductible.
Beaucoup des résultats ci-dessus sont des analogues de résultats sur les entiers :
Entiers relatifs | Polynômes |
---|---|
Addition des entiers | Addition terme à terme des coefficients |
Multiplication | Multiplication étendue à par distributivité |
Division euclidienne avec critère sur la valeur absolue du reste | Division euclidienne avec critère sur le degré du reste |
PGCD : Plus grand diviseur en valeur absolue | PGCD : Polynôme diviseur de plus grand degré |
Calcul modulo un nombres premier (i.e. dans ) | Calcul modulo un polynôme irréductible (i.e. dans ) |
Les mathématiciens aiment bien identifier des structures communes entre différents ensembles et pouvoir généraliser les résultats et théorèmes sur ces structures.
Dans ce cas, la structure commune est celle des anneaux euclidiens, la notion de degré d’un polynôme étant l’analogue de la valeur absolue d’un entier.
La dernière ligne du tableau fait apparaître la structure d’anneau quotient.
Que nous disent les résultats ci-dessus ?
Ils nous disent que si l’on trouve un polynôme irréductible sur , en notant son degré, on pourra calculer sur les séquences finies comme on calcule sur : avec des symboles , , et
Tous ces résultats ont été obtenu en partant de , grâce à des algorithmes qui utilisent les opérateurs de (en particulier, l’algorithme de la division euclidienne).
Nous avons vu dans des articles précédents que les ensembles munis des opérateurs , , et avec certaines propriétés (en particulier, la distributivité de la multiplication) s’appellent des corps.
Les résultats obtenus ci-dessus peuvent donc se généraliser à n’importe quel corps (notamment et les corps finis de l’arithmétique modulaire, ainsi qu’aux nouveaux corps que nous allons construire à partir de ceux-ci).
Si l’on dispose d’un corps (notons le ) et d’un polynôme irréductible sur , on peut constuire un nouveau corps dont les éléments sont des séquences d’éléments de .
Les polynômes de degré sont irréductibles mais correspondent à des séquences de élément (on retrouve le calcul sur ). On s’intéresse donc aux polynômes irréductibles de degrés supérieurs.
Un tel corps est appelé une extension finie de .
On est donc amené à chercher des polynômes irréductibles de degré sur les corps que nous connaissons.
Le polynôme est irréductible dans , mais pas dans .
En effet, bien qu’on puisse le décomposer sous la forme en utilisant deux polynômes de degré , les coefficients de ces polynômes sont , pas tous rationnels.
Calculer avec des polynômes rationnels modulo revient à calculer avec des nombres de la forme où et sont rationnels.
Par exemple, en utilisant le type Fraction pour que les coefficients soient rationnels :
>>> from fractions import Fraction>>> P = X**2 - 2>>> (1 + Fraction(1, 2) * X) * (1 - Fraction(1, 2) * X) % PFraction(1, 2)
Ce qui traduit l’égalité .
💡 Un nombre comme qui est racine d’un polynôme à coefficients dans est un nombre algébrique.
Si est un nombre algébrique de degré (le degré minimal d’un polynôme rationnel annulant ),
on peut construire une extension algébrique de en comptant avec les nombres de la formes
où les coordonnées sont des rationnels.
Les calculs obtenus sont alors les mêmes que si on considère les séquences comme les coefficients d’un polynôme et qu’on
calcule modulo .
Il s’agit de trouver un polynôme à coefficients réels qui soit irréductible.
En cherchant des polynômes de degré , on trouve par exemple le polynôme .
Ce polynôme est irréductible car sinon on aurait une égalité de la forme avec et des nombres réels et donc .
On obtient en comptant avec des polynômes modulo les nombres complexes.
Mathématiquement, on définit donc (notation des ensembles quotient, qui est la transcription mathématique de ce qu’on vient de dire) qu’on appelle l’ensemble des nombres complexes.
Dans cet ensemble, on calcule avec des couples de nombres réels .
On appelle alors le couple , correspondant au polynômes . L’égalité est alors une autre façon de dire qu’on calcule modulo .
Le résultat suivant n’est par exemple que la traduction du calcul :
>>> (1 + X)**2 % (X**2 + 1)(0.0, 2.0)
En identifiant un point dans le plan, de coordonnées , au , on obtient les règles de calcul suivantes :
C’est-à-dire :
Le produit d’un polynôme par un autre modulo étant une opération linéaire, ceci permet de voir les nombres complexes comme des fonctions linéaires représentées par des matrices de la forme :
On en déduit alors l’interprétation géométrique des nombres complexes, telle qu’on la voit au lycée.
💡 Un théorème fondamental concernant les nombres complexes a pour conséquence qu’on ne peut pas constuire d’autres extensions finies de (par exemple, il n’est pas possible de définir une structure de corps sur les triplets de nombres réels…).
Nous avons construit dans l’article d’introduction à l’arithmétique modulaire les ensembles .
Nous avons vu que si est un nombre premier ces ensembles sont des corps, on privilégie alors la notation pour les désigner.
Dans le cas , on obtient un ensemble à deux éléments ( et ) sur lequel sont définies les opérations suivantes :
Addition
+ | 0 | 1 |
---|---|---|
0 | 0 | 1 |
1 | 1 | 0 |
Multiplication
× | 0 | 1 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
Si l’on identifie et à des booléens, l’addition peut-être vue comme l’opérateur logique ou exclusif et la multiplication comme l’opérateur logique et.
Ces opérations sont généralement supportées nativement par les processeurs, ce qui permet d’effectuer des calculs sur (et donc sur les polynômes à coefficients dans ) très rapidement.
Bien que les éléments de puissent être vus comme des booléens, je ne vais pas utiliser les booléens True
/ False
de Python
pour les implémenter.
En effet, en Python la classe bool
hérite de int
et les opérateurs numériques se comportent comme sur des entiers :
>>> True + True2
Définissons un type Mod2Number
pour représenter un élément de , comme il implémente les opérateurs numériques
des Number
nous pourrons l’utiliser pour créer des instances du type Polynomial
.
On se contente de remplacer l’addition et la multiplication par leurs équivalents modulo , l’inversion pow(x, -1)
est ici très simple puisque est le seul nombre inversible.
class Mod2Number(int):def __add__(self, other):if isinstance(other, int):return Mod2Number(super().__add__(other) % 2)return NotImplementeddef __radd__(self, other):return self + otherdef __neg__(self):return Mod2Number(super().__neg__() % 2)def __sub__(self, other):return self + (-other)def __rsub__(self, other):return -self + otherdef __mul__(self, other):if isinstance(other, int):return Mod2Number(super().__mul__(other) % 2)return NotImplementeddef __rmul__(self, other):return self * otherdef __pow__(self, other):if other < 0:if self == 1:return Mod2Number(1)else:raise ZeroDivisionErrorelse:return Mod2Number(1)
La fonction ci-dessous nous permettra de constuire un polynôme à coefficients dans à partir d’une chaîne de texte
représentant ses chiffres en commençant à gauche avec le poids le plus fort (notation big-endian, donc dans le sens
contraire de leur apparition dans le tuple
des coefficients).
def getMod2Poly(digits):return Polynomial(*(Mod2Number(d) for d in reversed(digits)))>>> getMod2Poly('100011101')(1, 0, 1, 1, 1, 0, 0, 0, 1)>>> getMod2Poly('100011101') * getMod2Poly('10101')(1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1)
Le polynôme est irréductible dans .
S’il ne l’était pas, on pourrait le décomposer comme produit de deux polynômes de degré (les degrés s’additionnent).
Les polynômes de degré dans correspondent à des couples de coefficients binaires dont le deuxième coefficient est non nul :
Les produits correspondant sont : , et .
Cette méthode par force brute suffit ici pour montrer l’irréductibilité (évidemment, on ne pourrait pas faire pareil sur un corps infini…).
En comptant avec les couples de chiffres binaires identifiés à des polynômes modulo on définit donc des règles de calcul qui font de cet ensemble un corps.
On peut calculer sa table de multiplication :
>>> P = getMod2Poly('111')>>> for a in range(4):>>> for b in range(a + 1):>>> a_digits, b_digits = f"{a:02b}", f"{b:02b}">>> prod = getMod2Poly(a_digits) * getMod2Poly(b_digits) % P>>> res = "".join(reversed([str(d) for d in prod])).zfill(2)>>> print(f"{a_digits} * {b_digits} = {res}")00 * 00 = 0001 * 00 = 0001 * 01 = 0110 * 00 = 0010 * 01 = 1010 * 10 = 1111 * 00 = 0011 * 01 = 1111 * 10 = 0111 * 11 = 10
Le polynôme suivant est également irréductible dans : .
Nous pouvons encore le prouver par force brute, mais cette fois-ci informatiquement. Il suffit de tester tous les polynômes de degré compris entre et , représentés en binaire par des entiers compris entre et .
>>> P = getMod2Poly('100011101')>>>>>> for i in range(2, 255):>>> T = getMod2Poly("{0:b}".format(i))>>> if (P % T).iszero:>>> print(T)>>> break>>> else:>>> print("No divisor found")No divisor found
L’existence de ce polynôme irréductible montre que l’on peut construire un corps dont les éléments sont les séquences de huit chiffres binaires, c’est-à-dire des octets. Ce corps est noté .
💡 En fait, on montre que pour tout nombre premier et pour tout entier , on peut construire un corps fini de taille , qui est une extension algébrique simple de .
Je vous laisse scanner le QR code ci-dessous et vérifier si vous arrivez à le lire, malgré le stylo que j’ai mis devant :
Je ne sais pas pour vous mais… chez moi ça marche !
Comment se fait-il que ce QR code soit encore lisible alors qu’une partie des données qu’il encode n’est pas visible ? Nous allons brièvement expliquer la technique utilisée, qui n’est d’ailleurs pas spécifique aux QR code.
Les QR code utilisent un quadrillage de carrés noirs et blancs pour stocker des données binaires.
Sur l’exemple ci-dessus, j’ai utilisé un QR code version 3, utilisant un quadrillage 29x29.
Différentes zones de ce quadrillage servent à stocker différentes informations :
Les métadonnées sont évidemment très critiques : si elles ne sont pas lisibles, il sera impossible d’interpréter le reste des données.
Je vous propose ci-dessous de voir comment fonctionne le code correcteur utilisé pour gérer les erreurs sur les métadonnées. Les codes correcteurs utilisés pour les données elles-mêmes sont un peu plus sophistiqués, je les évoque ensuite.
Les métadonnées d’un QR code sont codées sur cinq bits, par exemple .
On définit :
Encodage
zfill
).Si l’on implémente cela sur notre exemple, on obtient la chaîne qui sera réellement stockée :
def encode(message, g):M = getMod2Poly(message)G = getMod2Poly(g)T = getMod2Poly('10')**G.degree * MR = T % Greturn ("".join(reversed([str(d) for d in T - R])).zfill(len(message) + len(g) - 1))>>> encode('01100', '10100110111')011001000111101
Décodage
En l’absence d’erreur de lecture, le message d’origine se retrouve en lisant les cinq premiers bits du message encodé. On le vérifie sur notre exemple ci-dessus, et cela se justifie par les propriétés de la division euclidienne utilisée.
Cependant, tout l’intérêt d’un code détecteur / correcteur d’erreur est de gérer les cas où les données sont altérées, par exemple lorsqu’un QR code est partiellement illisible.
Pour ce code simple, il est possible de précalculer tous les codes valides, puis de chercher celui qui est le plus proche du message lu, on renvoie alors le message correspondant qui est le préfixe de longueur cinq du code obtenu :
valid_codes = {encode(f"{i:05b}", '10100110111') for i in range(2**5)}def decode(p):if p in valid_codes:return p[:5]else:return min(valid_codes,key=lambda code: sum(1 for i in range(len(code)) if p[i] != code[i]))[:5]
Ce code permet de corriger jusqu’à trois erreurs :
>>> decode('011001000111101') # No error, OK01100>>> decode('111001000110101') # 2 errors, OK01100>>> decode('111001010110101') # 3 errors, OK01100>>> decode('101001010110101') # 4 errors, KO10010
Le code décrit ci-dessus est utilisé en pratique pour encoder une partie des métadonnées des QR code, qui sont stockées sur cinq bits.
En voici une implémentation en C bien plus efficace, puisqu’elle implémente les opérations au niveau binaire.
Il s’agit de la librairie libqrencode que j’ai utilisée pour générer le QR code plus haut.
Le type de code décrit au-dessus appartient à une famille de codes correcteurs appelés les codes BCH, inventés autour des années 1960 et très largement utilisés (DVD, disques durs, etc.).
Je n’ai parlé ici que du code utilisé pour les métadonnées, relativement simple car il repose sur un polynôme à coefficients dans .
Les codes correcteurs utilisés pour coder les données elles-mêmes reposent sur des polynômes à coefficients dans des corps plus grands, par exemple le corps dont on a montré l’existence plus haut et qui permet de compter avec des opérations sur les octets.
Un des résultats fondamentaux sur les corps finis, nécessaire pour bien comprendre le fonctionnement de ces codes, est l’existence d’un
élément tel que tout élément du corps peut s’écrire sous la forme (on dit que est un élément primitif).
En voici une preuve, formulée dans le langage de la théorie des groupes.
Pour en savoir plus sur les codes correcteurs et comprendre pourquoi ils sont efficaces (en terme de nombres d’erreur corrigées par rapport au surplus de données nécessaire), vous pouvez lire :
Maths et applications, avec les mains et avec du code 💻
Suivre sur Twitter