Complexes.
On a vu dans le chapitre précédent que pour Python, un nombre complexe "z" est essentiellement une structure abritant deux réels, accessibles par "z.real" et "z.imag" respectivement. La construction de Cayley-Dickson généralise ce point de vue : En prenant deux complexes "a" et "b", on peut les regrouper dans une nouvelle structure qui est considérée comme un nombre : Un quaternion.
Pour toute la suite, il est conseillé d'importer les fonctions du module "math" de Python :
from math import *
Mais en réalité, seule la méthode "hypot" sera utilisée, ce qui signifie que le minimum nécessaire était
from math import hypot
Quaternions.
Définition.
Toutes les méthodes (qui permettent de manipuler les quaternions) peuvent être regroupées dans une classe nommée "Quaternion" :
class Quaternion:
 def __init__(self,a,b):
 self.a=a
 self.b=b
La première méthode, l'initialisation, crée donc deux variables "a" et "b" (qui seront des complexes, mais Python ne le sait pas encore) et les rendre accessibles par la notation avec un point, les nombres "q.a" et "q.b" (les deux complexes qui définissent le quaternion "q") étant des propriétés du quaternion.
Affichage.
Pour y voir quelque chose, une méthode d'affichage est nécessaire. Comme Python en possède déjà une (la conversion en chaîne de caractères, ou "string", notée "__str__"), on va la surcharger :
 def __str__(self):
 aff='('
 aff+=str(self.a.real)+')+('
 aff+=str(self.a.imag)+')i+('
 aff+=str(self.b.real)+')j+('
 aff+=str(self.b.imag)+')k'
 return aff
Un quaternion possède deux propriétés qui sont des nombres complexes, mais chacun d'eux est formé de deux nombres réels, donc un quaternion est formé de 4 nombres réels, ce sont ceux qui sont affichés par la méthode ci-dessus, lorsqu'on entre par exemple "print(q)".
Fonctions.
Opposé.
 def __neg__(self):
 return Quaternion(-self.a,-self.b)
En écrivant "-q", on aura désormais l'opposé de "q".
Module.
 def __abs__(self):
 return hypot(abs(self.a),abs(self.b))
Grâce à cette méthode, "abs(q)" retourne un réel, la racine carrée de sa norme.
Conjugué.
 def conjugate(self):
 return Quaternion(self.a.conjugate(),-self.b)
"abs(q.conjugate())" renvoie le même résultat que "abs(q)" parce que tout quaternion a la même norme que son conjugué.
Opérations.
Addition.
Pour additionner deux quaternions, on additionne leurs "a" respectifs, et leurs "b" respectifs :
 def __add__(self,other):
 return Quaternion(self.a+other.a,self.b+other.b)
Soustraction.
 def __sub__(self,other):
 return Quaternion(self.a-other.a,self.b-other.b)
Multiplication.
Le produit de deux quaternions est plus difficile à définir :
 def __mul__(self,other):
 c=self.a*other.a-self.b*other.b.conjugate()
 d=self.a*other.b+self.b*other.a.conjugate()
 return Quaternion(c,d)
Ce produit admet le quaternion "Quaternion(1,0)" comme élément neutre, et il est associatif comme tout produit qui se respecte. Mais il n'est pas commutatif :
p=Quaternion(-2+1J,2+3J)
q=Quaternion(3-2J,5+1J)
print(p*q)
print(q*p)
Division.
Multiplication par un réel.
Pour définir le plus facilement possible le quotient de deux quaternions, on a intérêt à définir le produit d'un quaternion par un réel (on peut déjà l'effectuer avec la méthode de produit, en assimilant le réel "r" avec la quaternion "Quaternion(r,0)"). Mais comme le symbole de multiplication est déjà utilisé pour la multiplication des quaternions, il en faut un autre pour la multiplication d'un quaternion par un réel. Or il se trouve que "__rmul__" (multiplication à l'envers) est encore disponible, donc pour peu qu'on multiplie à droite dans la définition, et à gauche en pratique, on peut ajouter cette méthode :
 def __rmul__(self,k):
 return Quaternion(self.a*k,self.b*k)
Alors, pour tripler un quaternion "q", on peut faire, au choix, "q*Quaternion(3,0)", ou "3*q".
Division.
Le quotient de deux quaternions est désormais simple à définir :
 def __div__(self,other):
 return self*(1./abs(other)**2*other.conjugate())
Le quotient d'un quaternion par son conjugué est de norme 1 :
p=Quaternion(-2+1J,2+3J)
print(p/p.conjugate())
Cet exemple révèle que formula_1, ce qui revient à la décomposition suivante de 81 (un carré) comme somme de 4 carrés : formula_2.
Puissances.
Même si la multiplication des quaternions n'est pas commutative, elle est associative, c'est tout ce qu'il faut pour définir les puissances des quaternions à exposants entiers (et donc, par formules de Taylor, de la trigonométrie et des exponentielles de quaternions) :
 def __pow__(self,n):
 r=1
 for i in range(n):
 r=r*self
 return r
Par exemple, on peut calculer le carré d'un quaternion :
q=Quaternion(2J/7,(3+6J)/7)
print(q**2)
Plus généralement, l'ensemble des solutions de formula_3 est une sphère.
L'étude des itérés de formula_4 où "q" et "c" sont des quaternions, mène à des ensembles de Mandelbrot quaternionniques ()
Résumé.
Voici le contenu complet de la classe "Quaternion" de Python :
from math import hypot
class Quaternion:
 def __init__(self,a,b):
 self.a=a
 self.b=b
 def __str__(self):
 aff='('
 aff+=str(self.a.real)+')+('
 aff+=str(self.a.imag)+')i+('
 aff+=str(self.b.real)+')j+('
 aff+=str(self.b.imag)+')k'
 return aff
 def __neg__(self):
 return Quaternion(-self.a,-self.b)
 def __add__(self,other):
 return Quaternion(self.a+other.a,self.b+other.b)
 def __sub__(self,other):
 return Quaternion(self.a-other.a,self.b-other.b)
 def __mul__(self,other):
 c=self.a*other.a-self.b*other.b.conjugate()
 d=self.a*other.b+self.b*other.a.conjugate()
 return Quaternion(c,d)
 def __rmul__(self,k):
 return Quaternion(self.a*k,self.b*k)
 def __abs__(self):
 return hypot(abs(self.a),abs(self.b))
 def conjugate(self):
 return Quaternion(self.a.conjugate(),-self.b)
 def __div__(self,other):
 return self*(1./abs(other)**2*other.conjugate())
 def __pow__(self,n):
 r=1
 for i in range(n):
 r=r*self
 return r
Il suffit de placer tout ça dans un fichier appelé "quaternions.py" et disposer de toutes ces méthodes en important le module nouvellement créé par
from quaternions import *
Ce qui permet alors de déclarer des quaternions, puis d'effectuer des opérations dessus.
Octonions.
Ce qui est intéressant avec la construction de Cayley-Dickson utilisée ci-dessus pour les quaternions, c'est qu'elle se généralise : En définissant une structure (un objet) comprenant deux quaternions "a" et "b", on définit un octonion.
Définition et affichage.
Définition.
from math import hypot
class Octonion:
 def __init__(self,a,b):
 self.a=a
 self.b=b 
Affichage.
Comme formula_5 est de dimension 8 sur formula_6, l'affichage est plus compliqué que celui des quaternions :
 def __str__(self):
 aff='('
 aff+=str(self.a.a.real)+')+('
 aff+=str(self.a.a.imag)+')i+('
 aff+=str(self.a.b.real)+')j+('
 aff+=str(self.a.b.imag)+')k+('
 aff+=str(self.b.a.real)+')l+('
 aff+=str(self.b.a.imag)+')li+('
 aff+=str(self.b.b.real)+')lj+('
 aff+=str(self.b.b.imag)+')lk'
 return aff
On voit une arborescence apparaître, le "a.a.real" désignant la partie réelle du "a" du quaternion "a" de l'octonion. La notation avec les points prend ici tout son intérêt, permettant une concision pythonienne qui rendrait presque apprivoisés ces redoutables octonions!
Fonctions.
Les fonctions sur les octonions se définissent presque comme celles sur les quaternions, Cayley-Dickson oblige :
Opposé.
 def __neg__(self):
 return Octonion(-self.a,-self.b)
Module.
 def __abs__(self):
 return hypot(abs(self.a),abs(self.b))
C'est pour permettre cette concision qu'on a importé la méthode "hypot" du module "math".
Conjugué.
 def conjugate(self):
 return Octonion(self.a.conjugate(),-self.b)
Opérations.
Addition.
 def __add__(self,other):
 return Octonion(self.a+other.a,self.b+other.b)
Encore une fois, le fait d'avoir surchargé la méthode "__add__" de Python permet de noter simplement "m+n" la somme des octonions "m" et "n".
Soustraction.
 def __sub__(self,other):
 return Octonion(self.a-other.a,self.b-other.b)
Multiplication.
 def __mul__(self,other):
 c=self.a*other.a-other.b*self.b.conjugate()
 d=self.a.conjugate()*other.b+other.a*self.b
 return Octonion(c,d)
Non seulement la multiplication des octonions n'est pas commutative, elle n'est plus associative non plus :
m=Octonion(Quaternion(3+4J,2-7J),Quaternion(1+3J,5-3J))
n=Octonion(Quaternion(2+1J,1-3J),Quaternion(2-2J,1+1J))
o=Octonion(Quaternion(3-2J,-5+3J),Quaternion(1-2J,2-1J))
print((m*n)*o)
print(m*(n*o))
Division.
Grâce à la notion de conjugué, on peut facilement définir le quotient de deux octonions, mais c'est encore plus facile en multipliant un octonion par un réel :
Produit par un réel.
 def __rmul__(self,k):
 return Octonion(k*self.a,k*self.b)
Quotient de deux octonions.
 def __div__(self,other):
 return self.conjugate()*(1./abs(other)**2*other)
Là encore, le quotient d'un octonion par son conjugué est de norme 1 :
m=Octonion(Quaternion(3+4J,2-7J),Quaternion(1+3J,5-3J))
n=Octonion(Quaternion(2+1J,1-3J),Quaternion(2-2J,1+1J))
print(m/m.conjugate())
print(abs(n/n.conjugate()))
Ces calculs permettent de décomposer un carré en somme de 8 carrés.
Puissances.
Comme la multiplication des octonions n'est pas associative, les puissances des octonions ne présentent guère d'intérêt, sauf pour la puissance 2, et sur formula_5, l'ensemble des solutions de l'équation formula_8 est une sphère de dimension 6.
Résumé.
La classe "Octonion" de Python peut se résumer à ceci :
class Octonion:
 def __init__(self,a,b):
 self.a=a
 self.b=b 
 def __str__(self):
 aff='('
 aff+=str(self.a.a.real)+')+('
 aff+=str(self.a.a.imag)+')i+('
 aff+=str(self.a.b.real)+')j+('
 aff+=str(self.a.b.imag)+')k+('
 aff+=str(self.b.a.real)+')l+('
 aff+=str(self.b.a.imag)+')li+('
 aff+=str(self.b.b.real)+')lj+('
 aff+=str(self.b.b.imag)+')lk'
 return aff
 def __neg__(self):
 return Octonion(-self.a,-self.b)
 def __add__(self,other):
 return Octonion(self.a+other.a,self.b+other.b)
 def __sub__(self,other):
 return Octonion(self.a-other.a,self.b-other.b)
 def __mul__(self,other):
 c=self.a*other.a-other.b*self.b.conjugate()
 d=self.a.conjugate()*other.b+other.a*self.b
 return Octonion(c,d)
 def __rmul__(self,k):
 return Octonion(k*self.a,k*self.b)
 def __abs__(self):
 return hypot(abs(self.a),abs(self.b))
 def conjugate(self):
 return Octonion(self.a.conjugate(),-self.b)
 def __div__(self,other):
 return self.conjugate()*(1./abs(other)**2*other)
Pour peu qu'on l'ait enregistrée dans un fichier "octonions.py", il suffit pour pouvoir effectuer des calculs sur les octonions, d'importer ce fichier par
from octonions import *